What are types in TypeScript? This blog post describes two perspectives that help with understanding them.
The following three questions are important for understanding how types work and need to be answered from each of the two perspectives.
What does it mean for myVariable
to have the type MyType
?
let myVariable: MyType = /*...*/;
Is SourceType
assignable to TargetType
?
let source: SourceType = /*...*/;
let target: TargetType = source;
How is TypeUnion
derived from Type1
, Type2
, and Type3
?
type TypeUnion = Type1 | Type2 | Type3;
From this perspective, a type is a set of values:
If myVariable
has the type MyType
, that means that all values that can be assigned to myVariable
must be elements of the set MyType
.
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
.
The type union of the types Type1
, Type2
, and Type3
is the set-theoretic union of the sets that define them.
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
.Let’s consider the questions:
myVariable
has type MyType
if the static type of myVariable
is assignable to MyType
.SourceType
is assignable to TargetType
if they are assignment-compatible.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;
One of the responsibilities of a static type system is to determine if two static types are compatible:
U
of an actual parameter (provided, e.g., via a function call)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.
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
.
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