How do sets and types relate in TypeScript ?
What is a type ?
A type is set.
Let’s have a quick example:
type Binary = 0 | 1;
type Test = Binary extends number ? true : false; // true
0 | 1
is a subset of number
, in terms of set theory, denoted as follows:
{ 0, 1 } ⊂ number
What is assignability ?
B is assignable to A
can be transposed to B is a subset of A
.
const firstName: string = "Meg"; // ✅ {"Meg"} ⊂ string <=> "Meg" ∈ string
const binary: 0 | 1 = 6; // 💥 { 6 } ⊄ 0 | 1 <=> 6 ∉ { 1, 0 }
What does the extends keyword do ?
The keyword extends
is a check for assignability.
A extends B
can be transposed to A is subset of B
.
type Point2D = { x: number, y: number };
type Point3D = { x: number, y: number, z: number };
type isPoint<A> = A extends Point2D ? true : false;
type C = isPoint<Point3D>; // true
When are two sets equal ?
if A contains B and B contains A, then A=B
type Dual = true | false;
type C = boolean extends Dual ? 'Yes' : 'No' // 'Yes'
type D = Dual extends boolean ? 'Yes' : 'No' // 'Yes'
// Dual and boolean are equal sets
Why more detailed objects describe narrower set ?
Let’s re-take our example up above.
Point3D
is a subset of Point2D
, which can be transposed as follows:
{ x: number, y: number, z:number } ⊂ { x: number, y: number }
{}
type generates all possible objects type, recursively.
Why using intersection for merging objects type ?
Since {}
is the most generic object type, more specialized objects type describe narrower subsets. As an example:
{ first: string } ⊃ { first: string, last: string, age: number }
{ last: string } ⊃ { last: number, salary: number, first: string }
Therefore, from all possible objects generated by {first:string}
and {last: string}
the intersection set of both are all objects whose keys are in common:
{ first: string } ∩ { last: number } = { first: string, last: string }
What is any ?
any
is a top type. This means that any
embraces all other types.
let article: any;
article = "The Two Towers"; // ✅
article = true; // ✅
article = { title : "The Two Towers", id: "8AB19U" }; // ✅
article = new Date(); // ✅
article = 55; // ✅
article = undefined; // ✅
article = null; // ✅
article = Symbol(1); // ✅
article = 739812n; // ✅
Why is any inconsistent ?
The issue with any
is that it does not comply with the set theory. Let’s illustrate that.
type B = any extends number ? 'Yes' : 'No' // 'Yes' | 'No'
any
— is assignable to a narrower set, which contradicts the set theory.
What is unknown ?
unknown
(TS 3.0) is a top type like any
.
let article: unknown;
article = "The Two Towers"; // ✅
article = true; // ✅
article = { title : "The Two Towers", id: "8AB19U" }; // ✅
article = new Date(); // ✅
article = 55; // ✅
article = undefined; // ✅
article = null; // ✅
article = Symbol(1); // ✅
article = 739812n; // ✅
How does unknown differ to any ?
In contrast to any
, unknown
enforces the developer to do type narrowing.
Type narrowing is the process of refining a type at a specific part of the code to ensure type-safety.
// `any` is like opting out of type-safety
const getYear = (date: any) => date.getFullYear();
const year0 = getYear(new Date("2023-01-23")); // ✅
const year1 = getYear(1674459749659); // 🐛 at runtime
// `unknown` enforces type narrowing
const getYear = (date: unknown) => date.getFullYear(); // 💥 TS error
const getYearOK = (date: unknown) =>
(date instanceof Date
|| typeof date === "string" // narrowing
|| typeof date === "number"
)
? new Date(date).getFullYear()
: 1970
const year0 = getYearOK("2016-03-12 13:00:00"); // ✅ 2016
const year1 = getYearOK(new Date("2014-03-12 13:22:00")); // ✅ 2014
const year2 = getYearOK(1316116057189); // ✅ 2011
const year3 = getYearOK([2007, 0, 29]); // ✅ 1970
What is never ?
never
(TS 2.0) is a bottom type.
type Binary = 0 | 1;
type Bool = 0 | 1 | never;
type C = Bool extends Binary ? 'Yes' : 'No' // 'Yes'
type D = Binary extends Bool ? 'Yes' : 'No' // 'Yes'
// `never` is the empty set, therefore Binary and Bool are equal sets
never
is the empty set, denoted by ∅ in the set theory.
What can never be used for ?
never
can be used for type filtering or exhaustive type checking.
type Food = "🍊" | "🍌" | "🥒" | "🍐" | "🍓" | "🥕" | "🍒" | "🍆";
// 💡 `never` is useful for filtering
type FilterOut<T, U> = T extends U ? never : T;
type Fruits = FilterOut<Food, "🥒" | "🥕" | "🍆">;
// "🍊" | "🍌" | "🍐" | "🍓" | "🍒"
What is structural typing ?
TypeScript cares about the minimal shapes of types. This goes in pair with what we saw up above about {}
generating all possibles objects type.
type Point2D = { x: number, y: number };
type Point3D = { x: number, y: number, z: number };
let point2D: Point2D = { x: 10, y: 10 };
let point3D: Point3D = { x: 0, y: 0, z: 20 };
point2D = point3D; // ✅
A side-note on that regard is the so-called excess property check.
const point2D: Point2D = { x: 10, y: -1, z: 10 }; // 💥 TS error (2353)
Excess property check
does not go against the structural typing. It’s more like a hint to the developer to prevent typos.
Wrap up
Types as sets is a good mental model to reason about types in TypeScript.
any
is mathematically inconsistent, so use unknown
instead.
If you’d like to go into more advanced TypeScript’s features, you may have a look at this article I wrote.