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.
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.
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.
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.
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