What is a type in TypeScript? Two perspectives

[2020-02-26] dev, javascript, typescript
(Ad, please don’t block)

What are types in TypeScript? This blog post describes two perspectives that help with understanding them.

Three questions for each perspective  

The following three questions are important for understanding how types work and need to be answered from each of the two perspectives.

  1. What does it mean for myVariable to have the type MyType?

    let myVariable: MyType = /*...*/;
    
  2. Is SourceType assignable to TargetType?

    let source: SourceType = /*...*/;
    let target: TargetType = source;
    
  3. How is TypeUnion derived from Type1, Type2, and Type3?

    type TypeUnion = Type1 | Type2 | Type3;
    

Perspective 1: types are sets of values  

From this perspective, a type is a set of values:

  1. If myVariable has the type MyType, that means that all values that can be assigned to myVariable must be elements of the set MyType.

  2. SourceType is assignable to TargetType is SourceType is a subset of TargetType. As a consequence, all values allowed by SourceType are also allowed by TargetType.

  3. The type union of the types Type1, Type2, and Type3 is the set-theoretic union of the sets that define them.

Perspective 2: type compatibility relationships  

From this perspective, we are not concerned with values and how they flow when code is executed. Instead, we take a more static view:

  • Source code has locations and each location has a static type. In a TypeScript-aware editor, we can see the static type of a location if we hover above it with the cursor.

  • When a source location is connected to a target location via an assignment, a function call, etc., then the type of the source location must be compatible with the type of the target location. The TypeScript specification defines type compatibility via so-called type relationships.

  • The type relationship assignment compatibility defines when a source type S is assignable to a target type T:

    • S and T are identical types.
    • S or T is the type any.
    • Etc.

Let’s consider the questions:

  1. myVariable has type MyType if the static type of myVariable is assignable to MyType.
  2. SourceType is assignable to TargetType if they are assignment-compatible.
  3. How type unions work is defined via the type relationship apparent members.

An interesting trait of TypeScript’s type system is that the same variable can have different static types at different locations:

const arr = [];
// %inferred-type: any[]
arr;

arr.push(123);
// %inferred-type: number[]
arr;

arr.push('abc');
// %inferred-type: (string | number)[]
arr;

Nominal type systems vs. structural type systems  

One of the responsibilities of a static type system is to determine if two static types are compatible:

  • The static type U of an actual parameter (provided, e.g., via a function call)
  • The static type T of the corresponding formal parameter (specified as part of a function definition)

This often means checking if U is a subtype of T. Two approaches for this check are (roughly):

  • In a nominal or nominative type system, two static types are equal if they have the same identity (“name”). One type is a subtype of another if their subtype relationship was declared explicitly.

    • Languages with nominal typing are C++, Java, C#, Swift, and Rust.
  • In a structural type system, two static types are equal if they have the same structure (if their parts have the same names and the same types). One type U is a subtype of another type T if U has all parts of T (and possibly others) and each part of U has a subtype of the corresponding part of T.

    • Languages with structural typing are OCaml/ReasonML, Haskell, and TypeScript.

The following code produces a type error (line A) in nominal type systems, but is legal in TypeScript’s structural type system because class A and class B have the same structure:

class A {
  name = 'A';
}
class B {
  name = 'B';
}
const someVariable: A = new B(); // (A)

TypeScript’s interfaces also work structurally – they don’t have to be implemented in order to match:

interface Point {
  x: number;
  y: number;
}
const point: Point = {x: 1, y: 2}; // OK

Further reading