How do sets and types relate in TypeScript ?

Thomas Rubattel
5 min readJul 7, 2024

--

{} type generates all possible objects type, recursively

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.

--

--