Skip to content

The Type-System Mindset

Here is the reframe that unlocks TypeScript. Stop thinking of a type as a label and start thinking of it as a set — the collection of every value that legally belongs to it.

  • boolean is the set { true, false } — exactly two members.
  • undefined is a set with one member: { undefined }.
  • string is the (infinite) set of all strings.
  • 42 — yes, a literal — is a set with one member: { 42 }. This is a literal type.

Once types are sets, the operations you already know from math explain the whole system.

a is assignable to b if and only if the set a is a subset of the set b. That’s the entire rule.

let x: 42 = 42;
let y: number = x; // ✅ {42} ⊆ all numbers
let a: number = 42;
let b: 42 = a; // ❌ all numbers ⊄ {42}
// Error: Type 'number' is not assignable to type '42'.

The literal 42 fits into number because {42} is a subset of all numbers. The reverse fails because most numbers aren’t 42. You never have to memorize assignability rules — you just ask “is this set inside that set?”

Unions are set union; intersections are set intersection

Section titled “Unions are set union; intersections are set intersection”
flowchart TB
  subgraph u["A | B  (union = OR)"]
    ua["string"] --- ub["number"]
    note1["a value in EITHER set
→ more values, wider"]
  end
  subgraph i["A & B  (intersection = AND)"]
    note2["a value in BOTH sets
→ fewer values, narrower"]
  end
Types as sets: union widens, intersection narrows
  • string | number is the union of the two sets — a value that’s in either. Unions are wider (more members).
  • A & B is the intersection — a value that’s in both. For object types this means “has all properties of both,” which is why intersections narrow.

A common surprise falls right out of this: string & number is the empty set. No value is both a string and a number — so its type is never.

Sets have a smallest and largest, and TypeScript names them:

  • never is the empty set { } — no values at all. Nothing is assignable to never (you can’t produce a member of an empty set), but never is assignable to everything (the empty set is a subset of every set). It’s what a function that never returns is typed as, and the signal that a branch is unreachable.
  • unknown is the set of all values — the top type. Everything is assignable to unknown, but unknown is assignable to almost nothing until you narrow it. It’s the type-safe version of any.
let n: never;
let u: unknown = "anything goes";
let s: string = u; // ❌ unknown is too wide — narrow it first
let x: unknown = 42; // ✅ everything fits into unknown

any breaks this model on purpose: it opts out of set-checking entirely, which is why it’s dangerous. unknown keeps the model and forces you to narrow.

In the "types as sets" model, when is type A assignable to type B?
What is the type of `string & number`, and why?
What set does `never` represent?
How does `unknown` differ from `any`?