loading...
Cover image for TypeScript?.. or JavaScript?

TypeScript?.. or JavaScript?

f1lt3r profile image Alistair MacDonald Updated on ・3 min read

JavaScript! Why?

  1. Better composability
  2. Faster development

Type Safety Bugs

I spend about 1% of my time dealing with types and type related bugs in JavaScript. To be fair, I use Prettier, ESLint, & BDD with VS Code, so most type safety bugs get knocked down before production anyway.

Eric Elliot has a good post on this called, The TypeScript Tax.

TypeScript is JavaScript's Aircraft Carrier

To draw an analogy, JavaScript is a jump-jet and TypeScript is an aircraft carrier; And even though TypeScript is safe, it's still not safe enough to run a nuclear power plant.

Classes

TypeScript's classes don't get me excited, because I don't use classes. I prefer JavaScript object composition.

Enclosed Pure Object Composition

I like to use Enclosed Pure Object Composition in place of Object Oriented Programming. Conceptually, the mental model is exactly the same, but with the added powers that first-class functions absorb in the Web's event based architecture. Everything I need from OOP can be done with plain JavaScript. No prototypes. No classes necessary.

For example: here is the Enclosed Object equivalent of a Class:

// Counter.mjs

export const Counter = (count = 0) => ({
  add: () => (count += 1),
  get count() {
    return count;
  },
});

const counter = Counter(2);
counter.add();
counter.add();
console.log(counter.count); // 4

This kind of object composition is easier to reason about. The memory footprint and CPU load of Object Composition is identical to Classes and Prototypes.

Let's compose...

// Counters.mjs

import { Counter } from './Counter.mjs';

export const Counters = (...counters) => ({
  add: () => counters.map((counter) => counter.add()),
  get count() {
    return counters.map((counter) => counter.count);
  },
});

const counters = Counters(Counter(0), Counter(1));
counters.add();
console.log(counters.count); // [ 1, 2 ]

Extensible Object Composition

We can make our pattern more extensible. Here is a similar object composition, allowing for the use of JavaScript's this keyword.

// Employee.mjs

const methods = () => ({
  work() {
    this.product += this.productivity;
  },

  report() {
    console.log(
      `I'm ${this.name}, a ${this.role}.
       I produced ${this.product} units.`
    );
  }
});

export const Employee = name => ({
  name,
  role: 'worker',
  productivity: 2,
  product: 0,
  ...methods()
});

const al = Employee('Al');
al.work();
al.report();

// I'm Al, a worker. I produced 2 units.

Let's extend...

// Manager.mjs

import { Employee } from './Employee.mjs'

const accept = () => ({
  accept({ role, productivity }) {
    Object.assign(this, {
      role,
      productivity
    });
  }
});

const al = Object.assign(
  Employee('Al'),
  accept()
);

const promotion = {
  role: 'manager',
  productivity: 1
};

al.accept(promotion);
al.work();
al.report();
// I'm Al, a manager. I produced 1 units.

JavaScript's this keyword is unnecessary. The same result can be achieved by passing the employee's state to the scope of the employee's methods.

// Employee.mjs

const work = state => ({
  work: () => {
    state.product += state.productivity;
  }
});

export const Employee = name => {
  const employee = {
    name,
    role: 'worker',
    productivity: 2,
    product: 0
  };

  return Object.assign(
    employee,
    work(employee)
  );
};

const al = Employee('Al');
al.work();
console.log(al.product); // 2

Anti-Fragile

Object composition in Vanilla JavaScript is anti-fragile. I don't have to keep changing my code when the language's class API surface shifts. I don't have to get things working again when packages in TypeScript's Node ecosystem deliver breaking changes, in exchange for fancier features or security enhancements. (This is not an anti-security statement).

Keep The Web Simple

I often wonder how many frontend engineers learn frameworks, libraries and supersets, yet never realize the awesome power of modern JavaScript.

I love writing pure, enclosed objects, wrapped in the lexical scopes of first class functions, all the way down; There's Very little magic, and a whole lot of beauty.

If you want learn more about the inner workings of the code patterns above, read Kyle Simpson's excellent book series called, You Don't Know JS (Yet).

The following three books are particularly helpful:

  1. Scopes and Closures
  2. This and Object Prototypes
  3. ES6 & Beyond

You Don't Know JavaScript - Book Set

Posted on by:

f1lt3r profile

Alistair MacDonald

@f1lt3r

20 years 👨‍💻 sites & 📱 web-apps 👨‍🔬 Raspberry PI & eInk 👷 mech keebs 👐 Open Source 🕓 W3C Audio Working Group Chair 🦖 Data-Vis w/ Mozilla ❤️ JavaScript ♾

Discussion

markdown guide
 

While TypeScript fans are blasting him for it, it says something that Ryan Dahl (creator of Node.js and Deno) went from TS to JS for Deno's internals.

I have warned about every reason he gave for the switch for years, and then some.

*Edited out "fanboys," because there are plenty of valid reasons to consider TS and there were some well-reasoned arguments against his complaints; however, those were few compared to "Jeez, this guy" (again, creator of Node and Deno) "is an idiot! How doesn't he see that treating JS like C# is so much better!"

 

That's really interesting. Do you have links to any of that information? I would love to read what his reasons were.

 

You're welcome. It was quite vindicating to learn that someone who has made my career so much better with his creations has some of the same concerns about TS that I do.

Most of my other beefs with TS stem from what it's doing to JS devs:

  • stunting their growth in learning JS proper, whilst simultaneously
  • adding a great deal to their learning curve which is NOT actual JS,

and giving a false sense of security in:

  • static types that are transpiled away, when JavaScript has its own dynamic typing system you'd do well to learn, and documenting them can be as simple as JSDoc @params (you can even exploit tscheck in VS Code to check them as you write)
  • hacking around JS to treat it like some other class-based language e.g. C# (if you remember the "object oriented JavaScript" craze that came along before the first edition of YDKJS - which Simpson covers therein to some extent - you know why such a pattern of hacking around JS, rather than understanding what it does and why, is a road to sadness).

Finally, on the hiring end, because:

  • requiring it automatically cuts the available talent pool in half (and that's being generous, if you believe the oft-referenced Stack Overflow developer survey claiming that 50% of JS devs use TS).

Oh, and a comment about most annoying language replying to a post asking about hardest language - the commenter chose Java as most annoying for the same main reason I dislike Java, and yet another beef I have with TS:

  • verbosity.

Of course, if you follow the rainbow to the end, you'll notice the disclaimer at the top of the Design Doc that the Deno decision was discussed around.
Design Doc: Use JavaScript instead of TypeScript for internal Deno Code

tl;dr: Context is everything and know your use case. I'm still a TS newb, but I've already run into instances where TS removed burden by increasing transparency and simplifying apparent complexity in a manner similar to Artur's(@dotintegral) comment.

Yes, and still, not being a TS noob, I've run into the same issues for years.

FWIW the complexity issue is something many experienced JS devs likely complained about back in the early-mid part of the last decade when so many got caught up in the "object oriented JavaScript" craze (because it wasn't already? 🙃) that Kyle Simpson touched on in the first edition of You Don't Know JS, and the related "MVC EVERY CODEBASE PURELY FOR THE SAKE OF MVC 🙃" craze.

What a lot of us witnessed back then was a bunch of resulting code that was, somehow, even less readable and maintainable than it was in its previous spaghetti form (which we had probably already warned was spaghetti, but new hotness won out over common sense as the solution).

Right. Other than a clear model for componentization, it's hard to think of anything libraries offered that I wasn't already doing in one form or another. JSX/Hyper-HTML has also been quite revolutionary in it's own right, but on the other hand, that does come at a cost. I do like that the React community brought an awareness of classic CS concepts like immutability, side-effects, memoization, etc.

I could be very wrong here, but... I think the industry will dramatically shift back away from mega-frameworks and build-systems this decade, mostly due to new browser capabilities like Web Components and Modular JavaScript. I can't wait, it's going to be clean again for a while.

I actually don't have a problem with React - now Redux's obsession with pure functions to the point that you need either band-aidy (Thunk) or even more super-duper-boilerplatey (Saga) middleware just for async is a different story - in fact, it's my favorite library (actually React Native on mobile web and desktop, it's amazing what you can do with a single codebase without relying on a WebView these days).

I actually dug pretty deeply into HyperHTML (and other smaller libraries like Riot, and strictly relying on modern vanilla JS) before giving React a serious, unbiased chance, but I kept running into wheels that needed to be reinvented.

I haven't experienced many tradeoffs with JSX, especially now that there's hardly any reason for me to worry about repeating class/lifecycle methods (or using classes at all) ever again.

The thing that keeps me from web components is the same thing I ask any time I hear about use of just about any web framework/library: What's your mobile strategy? I don't like fragmenting teams to build what is often the same exact app in multiple codebases, if it's not absolutely necessary.

 

I feel the opposite. Recently I was working on a big project with advanced FP patterns. We composed and composed a huge amount of functions that were processing our monads. Sometimes, despite trying to keep everything simple, it's easy to get lost in big functions compositions. Especially when you enter a file for the first time and just want to make a small change, but you're unsure what's the structure at given point. Then comes TS and helps you with understanding quickly what's going on.

Though TS is not perfect and will not reduce run-time bugs (that's why we used monads) it can be helpful and improve your dev experience. The problem with TS is that you need to understand it's strong and weak parts to use it efficiently. It's the exact same thing as with JavaScript - it gives you a huge amount of tools, but it doesn't mean you should be using all of them. TS can bring you value, but you need to have a bit experience. Perhaps that is why a lot people are saying TS is crap. Because they haven't got to learn the good and bad parts and quit in the middle.

In TS, I don't really care about the types themselves. They are just a first step for what it brings. It helps you understand what are the structure of the objects and what are the arguments and return values of your functions. This is where it is most effective. For example, when working with pretty nested object - like the responses you get from hedless CMSes like Contentful - it's really easy to make a type-o or to refer to object.name instead object.title This is where TS comes and makes your experience much more bearable.

In addition to that, if configured properly (that is with strict mode on) it can warn you that when calling object.something() your object can be nullish. Though (as mentioned before) it does not completely eliminate run time errors, allows to reduce a bigger part of them.

It can be tedious, it can be annoying, but also it can help you a lot.

 

Finally someone with a Typescript preference! :) good points. It would be surprising if Object Composition itself was responsible for getting people lost, are you certain it wasn't an organizational issue?

 

Well, when you have 50+ contentful models with references to other models then yeah... From the data modeling point, the model makes sense. So I would argue that no, it was not an organisational issue. And honestly, if I have big project that I need to work on, I don't really want to spend precious brain power to memorize all of the models. I let TS hint me with fields ;-)

Make sense. TS hinting is pretty great. Hinting tends to drive me nuts, but it is really useful in a larger/unfamiliar codebases. VS Code has some level of hinting for regular JavaScript. I find that I am still able to traverse the model to a satisfying degree (see image).

VS Code Hinting, Traversal, Peeking in Vanilla JS Projects

JS Hinting is great, but it can only go as far as to the code/objects that you explicitly define. There is no way (unless I missed something) for VS Code to predict the structure of data incoming from API. With TS, you can just state that you expect data of this structure and hinting will work correctly. Granted, there is still possibility that you made mistake in typing the response, or the response changes after some time, but the same problems would occur in normal JS.

 

I spend about 1% of my time dealing with types and type related bugs in JavaScript.

I resonate with this. If it ain't broken, why bring in an entire language to fix it? TypeScript makes the problem of types in JavaScript bigger than it is. Also, thanks for the link to Eric Elliot's article; good reads all around.

 

Thanks. That makes sense. In my experience, it has been mainly junior engineers making a big deal about types. I get it. It's hard to understand the quirks without enough experience using JavaScript. But I don't think that makes typed languages easier. Perhaps it's a matter of taste for the most part, but I would guess if those junior engineers really learned modern JavaScript, they'd question whether they really wanted to pay the TypeScript tax. Building an identical project in both would be a great lesson in trade-offs.

 

Really enjoyed the kind of code you've written! Is there a book or course that teaches this type of OOP in modern JavaScript?

 

Thanks Ankush. The books mentioned in the bottom of the post will teach you some of the internals that make these patterns possible, as well as showing you some of the patterns. Another place that has information on this are the ui.dev courses: ui.dev/javascript-inheritance-vs-c...

 

You know, I have been through those books a while ago. While I did learn some core concepts, I don't think they teach any particular paradigm, especially not the one you've shown. But maybe they did and I've just forgotten stuff. I'll have a look again!

I do remember those books teaching... closures, lexical scope, prototypes, the module pattern, and classes, but I think you're right, I don't remember them talking about using object composition in place of OOP.

My point is, it's one thing to teach all the core concepts, and it's quite another to say, "Hey, you've been doing all the OOP stuff wrong! Here's how we can do it cleanly in JS" and then show how as a gradual buildup how we can create more complex but cleaner hierarchies, mini-frameworks, etc. Anyway, I guess this comes with practices and experience. 😇

Yeah, you make a good point. I hope I'm not coming across as saying other people are doing it wrong. What I'm trying to do give an opinion on why an alternative that's well worth the trade off.

FWIW, the second edition is in the works right now. It's now titled "You Don't Know JS Yet," and Simpson does promise to go into more detail and cover more than before (he's even going to cover ES6 classes with an honest effort on how-to vs personal opinion, despite personally being opposed to them).

I hope I'm not coming across as saying other people are doing it wrong.

No, no, I'm not saying that at all! I'm kind of tired of classical OOP and am always very happy to see/learn alternative approaches that lead to cleaner and more flexible code.

 

I like your demo of compo-sable JavaScript objects but....

I don't have to keep changing my code when the language's class API surface shifts.

No upgrade in any language (including JavaScript) should be done with reading the breaking changes section.

I don't have to get things working again when TypeScript's Node ecosystem delivers major version bumps, in exchange for fancier features and security enhancements. (This is not an anti-security statement).

No upgrade in any language (including JavaScript) should be done with reading the breaking changes section.

Both these arguments are User errors.

 

Yes, I do agree in principle. But it's not always that simple: github.com/Microsoft/TypeScript/is...

It's not that the language would break something, though it may, it's more that different parts of the ecosystem surrounding the language will shift at different rates. Some people will follow SemVer some people will not.

The number of modules and packages being installed in today's projects is astronomical. It's a lot of work to keep everything in sync. I would love to get real stastistics on time-cost of library incompatibilities for the Node.js universe.

 

We've all heard the Kyle Simpson quote (usually parroted without understanding) that "ES6 classes are syntactic sugar."

I've long taken it further than that. ES6 classes are a CONCESSION on the part of the ECMAScript committee to (TypeScript and) developers coming from class-based OOP languages who couldn't be bothered to properly understand the what and why of a prototype-based (also OOP) language.

 

That's interesting. When I wanted to learn OOP back in 2007, I started learning JavaScript Prototypal Inheritance. I didn't understand a thing I was reading. I had to switch over to Java to learn OOP, which was much clearer. Then once JavaScript Prototypes began to make sense to me, I started to realize that I was not really learning OOP in the traditional sense; JavaScript's composition model was meant to be different, but people were not getting it.

Prototypes are awesome, and I used them for a long time before discovering cleaner alternatives. I was giving a presentation on Web Audio at the Wakanday Conference in Boston, in 2013, where Douglas Crockford, the inventor of JSON, introduced the audience to the power of JavaScript Closures. That was the end of OOP for me.

If I remember correctly, this is the video of Douglas Crockford's talk here: youtube.com/watch?v=gz7KL7ZirZc

 

Great stuff @Alistair! I wonder, are there some examples where we can take your Counter function and add some additional functions inside of it via a mixin

 

Here is one possibility... though I had to cheat by adding the set count(value) setter. There may be a better way, but even if there is, I would use the longer form of extensible composition as it's easier to manipulate after the fact because the scope is passed on through state. But this method does demonstrate a private variable within the lexical scope of Counter.

const Counter = (count = 0) => ({
  add: () => (count += 1),
  get count() {
    return count;
  },
  // Set had to be added
  set count(value) {
    count = value;
  }
});

const counter = Counter(0);
counter.add();
console.log(counter.count); // 1

const subtract = () => ({
  subtract() {
    this.count -= 1;
  }
});

// The Subtract Mixin
Object.assign(counter, subtract());
counter.subtract();
console.log(counter.count); // 0
 

Exactly what the doctor ordered! I love it, I feel this avoids the pitfalls and complexities of class-based inheritance

Here you go Adrian-Samuel, this is how to extend the counter with mixins, without having to update the original object...

const Counter = (count = 0) => ({
  add: () => (count += 1),
  get count() {
    return count;
  }
});

let counter = Counter(0);
counter.add();
counter.count;
console.log(counter.count); // 1

const subtract = () => ({
  subtract() {
    this.count -= 1;
  }
});

const multiply = () => ({
  multiply(amount) {
    this.count *= amount;
  }
});

const extend = (base, ...mixins) =>
  Object.assign(
    {
      ...base
    },
    ...mixins.map(method => method())
  );

counter = extend(counter, subtract, multiply);
counter.subtract();
counter.subtract();
console.log(counter.count); // -1
counter.multiply(2);
console.log(counter.count); // -2

This is superb! Alistair, really appreciate you taking the time and effort to demonstrate these very useful examples to me!

 

Care to expand on your methodology with some examples? Do you work with a team? Are you writing JS for web apps? sites? node?

 

Good call. Added something very simple to give the basic idea.