Swatinem Blog Resume

Inadequacies of typed JavaScript

Some things can’t really be fixed :-(

— 7 min

The last week I have been playing extensively with both flow and TypeScript. And I have noticed two things that I consider to be bugs in both of them. So is there something wrong with my expectations maybe? Lets analyze both of the cases.

# non-nullable class members

TypeScript (TS) introduces non-nullable types with version 2, and flow has had them for quite a while I think. There have been tons of stories about how bad the null type/value is. Some call it the worst mistake in computer science. And indeed, most of the errors we have at runtime are related to null. Accessing properties of null, or calling null. Having to check values for null all the time is tedious and as the runtime errors prove, null can slip through at all kinds of places.

It is even worse in JavaScript because it does not have one null type but actually two: null and undefined. To make things worse typeof null === "object" and typeof undefined === "undefined, so you can’t even check for both cases at the same time; well actually you can use value == null since that also works for undefined. Go figure.

To make more worse still, JS also has boolean coercion. Which itself can be convenient, but you can very easily trip up. If you have a value that has the type number | null, using a simple if (value) will fail for the value 0. Probably not the thing you intended. And you will get it wrong at some point. I have done so over and over again.

So, to come back to the topic at hand. Both flow and TS now prevent you from tripping up on most of the null issues, except for the boolean coercion. But both of them get one case wrong:

class A {
  prop: string;
}
const a = new A();
a.prop.length; // typechecks just fine, fails at runtime

This simple code will typecheck just fine in both flow and TS, but will fail at runtime with a TypeError because you are accessing a property on null. I have reported this on the TS issue tracker but it was closed as a duplicate of a wontfixed issue. Apparently it is too difficult to correctly cover all the different ways in which constructors can behave.

As far as I see it, the root of the problem is twofold. First, JS objects were never meant to have a guaranteed shape, its only these type checkers that kind of try to enforce those things. And the second problem is that JS constructors work by assigning/creating properties on this, which can be aliased or delegated to other functions, etc…

In other languages with stricter guarantees, such as Rust, you do not have this or constructor functions at all. The language itself guarantees that creating an object of a certain type is atomic via an object literal that is guaranteed to include all properties. (You can still use object spread for convenience)

# Ideas to solve the Problem

Both flow and TS can hold up the typesystem guarantees if you are using object literals instead of classes. Use plain functions and object instead of classes. I have heard that one before. The whole functional programming crowd advocates this. Maybe they have a point? But I do like having methods on objects/prototypes. So maybe we can combine object literals that are correctly checked for null properties with methods somehow. Well there is this special __proto__ property that was specified for ES2015. I tried using it with TS, but failed. The method works perfectly in JS, but typechecking in TS fails. Well unless I want to copy every method into the object, which completely defeats the purpose of shared prototype methods.

type A = {
  a: string;
};

function A(a: string): A {
  return {
    __proto__: AProto, // can’t assign to unknown property
    a
  };
}

const AProto = {
  method(this: A) {
    return this.a.length;
  }
};

const a = A("a");
a.method(); // can’t call unknown method

So no luck here :-(

# Aliasing literal / union types

Both flow and TS have the notion of literal types and union types. Union types are really simple. The type A | B can either be A or B. But since those are not native to JS, you have to have some way to actually assert the type at runtime. For primitives its easy, since you can just check via typeof. But discriminating objects needs to be done manually with a property used to tag the object. In the following example, U is such a union type and it can be discriminated by looking at the type property. And here we are actually using strings as types: literal types. So in this case U.type has the type "a" | "b", so it can either be the string "a" or "b", but nothing else.

type A = { type: "a"; propA: string };
type B = { type: "b"; propB: number };
type U = A | B;

Both flow and TS handle these things quite well. If you use .type in a switch or if statement, you can match for the exact type. But both typecheckers fail if you want to alias that property, for example via destructuring: const {type} = X;. Here, the type of type is widened to string and it can no longer be used to discriminate the union type. I also reported this in the TS issue tracker and got the answer that is would be two complicated to implement this, because it would involve tracking the dependency between the two variables, increasing the depth of the dependency as more aliases are added.

Well at least this can be easily fixed by just always using the property instead of extracting it into a local.

# Closing remarks

I wish one of the compile-to-js languages would actually support real union types and has a real match expression. Automatically tagging variants or even supporting null-pointer optimization so you don’t have to have unnecessary indirections if a simple typeof check would be enough.

There are already compile-to-js languages that support things like this. For Example Elm and PureScript (both based on Haskell). And possibly ClojureScript, even though I know too little about that language. With the recently released BuckleScript, you can also compile OCaml to JS, with quite readable code, although I don’t quite like that it encodes structures as arrays and thus misses out on hidden class optimizations of JS engines (I could be wrong though). With Reason you can even write readable OCaml.

And of cause, I am very excited to see steady, although very slow progress towards compiling Rust to WebAssembly. Although I’m unsure about the FFI between WebAssembly/asm.js and real JS. It relies on a preallocated ArrayBuffer as a kind of heap, which might be good for low level performance but does not play well with consuming and outputting plain JS objects.

So the future is still very much open. I really hope there will be a readable, convenient, fast and safe compile-to-js language that can solve all these problems and integrates seamlessly into existing JS projects. One can dream though.