Typescript 2.x Gotchas

TypeScript is a language, developed by Microsoft, that is a superset of JavaScript and allows one to gradually transition over to a typed language. While it offers many nice features, there are some gotchas that one should be aware of when writing it. Most of the gotchas are inherited from trying to be compatible with JavaScript and supporting this gradual transition. This is an incomplete list of gotchas.

The Type Might Not Be True

Playground link.

const x: any = 1;
const y: string = x;

alert((typeof y) + " " + y);

This code will show "number 1". Despite y being type string. In many other languages, one might expect that the type of a variable is always the actual type of a variable and the value might be nonsense. But in TypeScript, it's really just a layer above a dynamically typed language, during run-time it is letting the underlying language do whatever it would do. The type any can be assigned to a variable and the compiler always allows the conversion.

This behaviour can be confusing in many situations. Many APIs start out (or are implicitly inferred) to have the type any so one might be using an API, thinking it is type safe, but in reality it is not.

The best practice described on the internet for dealing with JSON APIs suggests making an interface that describes the API then casting the JSON to the interface. In pseudo-code, something like:

interface Response {
    x: number;
    y: string;
}

const resp: Response = doQuery().json();

The problem with this is despite it type-checking, if the API call actually returns something different, one will not find out until the response is used somewhere else. Best practices in many other languages is to ensure that actual inputs match expected inputs on the boundary of a program.

Despite TypeScript saying that interfaces are structurally typed, interfaces don't actually have a type one can directly check against. The following code will output object.

Playground link.

interface Bar {
    x: string;
}

const y: Bar = { x: "hi" };

alert(typeof y);

Same for classes

Playground link.

class Foo {
    public x: number;
}

const y: Foo = new Foo();

alert(typeof y);

However, the compiler will at least prevent one from testing if the type of something is an interface, the following fails to compile (although the error is somewhat cryptic):

Playground link.

interface Bar {
    x: string;
}

const y: Bar = { x: "hi" };

alert(typeof y === "Bar");

In order to test if something is a Bar one would have to write a Type Guard. A Type Guard is a piece of code that the compiler can use to test if a value is a particular "type".

The Type Guard for Bar would look something like the following:

Playground link.

interface Bar {
    x: string;
}

function isBar(v: any): v is Bar {
    return typeof v === "object" &&
    "x" in v &&
    typeof v.x === "string";
}

const y: Bar = { x: "hi" };

alert(isBar(y));

However, Type Guards introduce their own complexity. They are tightly coupled to the type being tested and need to change with them. On top of that, they can be pretty laborious to write. Imagine writing the above Type Guard for a large interface and making sure it is always in-sync with that interface. Type Guards can also be wrong. The compiler does not prevent one from implementing the following Type Guard:

function isBar(v: any) v is Bar {
    return true;
}

It would be nice if TS could automatically generate a Type Guard. Or TypeScript could have a "checked cast" which would test if the type is actually the one it's being coerced into.

All of this is documented in TypeScript, the challenge is that it's not necessarily easy to know if a whole program is type-checking because the types are actually correct or pieces of it are being cast or making use of automatic any conversion, or a Type Guard is incorrectly implemented. A TypeScript program compiling is not a strong guarantee of its type correctness as one might traditionally expect. The concept of a "type" in TypeScript is not quite the same as many people working in statically typed languages might be used to: where a type is a proof. In TypeScript, types are closer to wishful thinking. They are likely better than no types at all but one has to adjust to them.

Looping

TypeScript offers two types of loops: for .. of .. and for .. in ... One of them loops over the keys of the container and the other loops over the values. Having two types of loops is fine however they are so similar that it is easy to confuse one with the other. Given the automatic type conversions that might happen in a program, it is possible to be iterating over the wrong thing, and since they look so similar pass a Code Review.

strictNullChecks and strictPropertyInitialization

One might expect that the --strictNullChecks option would ensure a strict null check. However the following code compiles with the option enabled but foo.x is clearly undefined.

Playground link.

class Foo {
    public x: number;
}

const foo: Foo = new Foo();

alert(foo.x);

This is because one actually needs --strictPropertyInitialization to cover this case.

It's probably best to just enable all of the safety compiler options.

Conclusion

In truth, TypeScript is two languages in one. It is JavaScript, and all of its semantics, with an attempt at another language layered on top. The language on top is meant to feel like a statically typed language with all of their guarantees, but concessions have to be made to smoothly integrate with the underlying language and the result is a whole new beast. If one goes into TypeScript thinking that this will give them the confidence using Rust or Haskell or Ocaml, they will be let down as well as possibly introducing subtle bugs. TypeScript needs to be understood, and appreciated, in its own light in order to be successful with it. >>>>>>> merge rev