Part 0 · 2 chapters · ~20 min

The Type System as a Language

Types as a lattice ordered by assignability, unknown at the top and never at the bottom, unions and intersections as bounds, structural typing and the one nominal-feeling rule, any as a hole, and the rules the checker applies case by case: objects, freshness, unions, functions, widening and generics, with the error message read from the bottom.

1

The type lattice

code
// the lattice, in code: what is assignable to what, and why
let u: unknown = 42            // everything goes in
// const n: number = u         // ✗ nothing comes out until narrowed
if (typeof u === 'number') { const n: number = u }   // ✓ narrowed (part 1)

function fail(msg: string): never { throw new Error(msg) }   // never: no value returns
const x: string = fail('boom')                               // ✓ never is assignable to everything (the line after is unreachable)

type Point = { x: number; y: number }
const p3 = { x: 1, y: 2, z: 3 }
const p: Point = p3                                          // ✓ structural: extra z is fine on a non-fresh value
// const q: Point = { x: 1, y: 2, z: 3 }                     // ✗ fresh literal: excess property z

type A = { a: string }; type B = { b: number }
const ab: A & B = { a: 'x', b: 1 }                           // intersection of objects: both shapes
type SN = string & number                                    // never: no value is both

const lit = 'ok'                                             // type "ok" (const keeps the literal)
let wide = 'ok'                                              // type string (let widens)
const tuple = [1, 'a'] as const                              // readonly [1, "a"]

declare const anything: any
const s: string = anything                                   // ✓ any is assignable to everything
const m: number = anything.foo.bar()                         // ✓ and every read of it is any: the checker has stopped
// prefer: declare const something: unknown; then narrow
assignability is the whole system
  1. The ordering: A ≤ B when every value of A is a value of B ("A is at least a B"). Literals below their primitives; a primitive below unions containing it; an object with more properties below one with fewer (more specific is lower); never below everything; everything below unknown. Every error is "A is not assignable to B" and the lattice says why.
  2. unknown is the top: anything goes in, nothing comes out until narrowed (part 1). The honest type for JSON, catch clauses and event payloads: it forces the check that any skips.
  3. never is the bottom: the type of no value. Functions that always throw, exhausted unions in a default branch (part 6), contradictory intersections. Assignable to everything because there is nothing to violate anything; a variable of type never means unreachable code or contradictory types.
  4. Unions and intersections: least upper bound and greatest lower bound. Objects intersect to a shape with both sets of properties; primitives intersect to never; unions of objects are narrowed by a discriminant.
  5. Structural: shape, not name; a class instance satisfies any interface its shape fits; excess property checks on fresh literals are the only nominal-feeling rule and exist to catch typos. Brands (part 6) simulate nominal typing when the domain needs it.
  6. any is not a position in the lattice but an escape from it: both top and bottom, contaminating every read and return. noImplicitAny stops inference of it; a lint stops you writing it; unknown is almost always what was meant.
THE TYPE LATTICE
assignability as an ordering, unknown at the top, never at the bottom, and where any breaks it
swipe the figure sideways, or tap expand for full screen
1/6
the ordering
The ordering: A ≤ B means A is assignable to B, read as "A is at least a B". number ≤ number | string; "a" ≤ string; { x: number; y: number } ≤ { x: number } (more properties, more specific, lower); never ≤ everything; everything ≤ unknown. The checker's assignability relation is this ordering computed structurally.
2

How assignability is checked

the rules, by case
  1. Objects: for every property of the target, the source has it with an assignable type; extras are fine; optional in the target may be absent; readonly is not part of assignability (a mutable value satisfies a readonly type).
  2. Freshness: an object literal written where a type is expected gets an excess property check; the same literal assigned to a variable first does not. The most common "why does it error here and not there".
  3. Unions: into a union, any member suffices; from a union, every member must. Subtype reduction collapses "a" | string to string, which is why hovers show less than you wrote.
  4. Functions: parameters contravariant (the target's parameter types must be assignable to the source's, because the function must handle whatever the caller passes), returns covariant, fewer parameters allowed, and under strictFunctionTypes function-typed properties are checked strictly while methods stay bivariant for compatibility (part 2).
  5. Literals and widening: const keeps the literal, let widens (the checker guessing you will mutate); as const keeps literals and makes readonly tuples; a returned object literal widens its literal properties unless annotated, which is why a discriminated union "does not narrow" (part 6).
  6. Generics: Box<S> ≤ Box<T> depends on how Box uses its parameter: covariant if only in outputs, contravariant if only in inputs, invariant if both. Measured from the definition and cached (part 2). The error message is a chain; the innermost line is the real reason, so read from the bottom.
the exercise
Take your longest TypeScript error of the week and read it from the last line up. Name the rule from this chapter at each line; the one you cannot name is the one to learn.
HOW ASSIGNABILITY IS CHECKED
the rules the checker applies to objects, functions, unions, literals and freshness
swipe the figure sideways, or tap expand for full screen
1/6
objects
Objects: S ≤ T when for every property p of T, S has p and S[p] ≤ T[p]. Extra properties in S are fine (S is more specific); a property optional in T may be absent in S; readonly in T does not stop a mutable S. So { x: 1, y: 2 } is assignable to { x: number } and a Point is assignable to any interface with a subset of its shape.