Advanced

Exhaustiveness Checking

Catch missing cases in switch statements using the never type and exhaustive unions.

What you will see

started

About 2 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

The `never` type proves exhaustiveness at compile time. To see the behavior at runtime, run the equivalent exhaustive switch and observe the catch-all message when a new variant slips through.

1

The never type

function assertNever(x) { throw new Error(`Unexpected: ${JSON.stringify(x)}`); } console.log("assertNever defined; throws if ever called with a real value");

Before you run it, say out loud what it will print. Then open the answer.

What it prints
assertNever defined; throws if ever called with a real value
2

Exhaustive switch

function area(s) { if (s.kind === "circle") return (Math.PI * s.r ** 2).toFixed(2); if (s.kind === "square") return (s.s ** 2).toString(); return assertNever(s); } function assertNever(x) { throw new Error("Unexpected: " + JSON.stringify(x)); } console.log(area({ kind: "circle", r: 2 })); console.log(area({ kind: "square", s: 3 }));

Before you run it, say out loud what it will print. Then open the answer.

What it prints
12.57 9
3

Default cases and errors

function handle(cmd) { if (cmd === "start") return "started"; else return assertNever(cmd); } function assertNever(x) { throw new Error("Unexpected: " + JSON.stringify(x)); } console.log(handle("start"));

Before you run it, say out loud what it will print. Then open the answer.

What it prints
started

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A union is fully enumerated in a switch; the exhaustiveness guard proves no case was left out at compile time.
1 Union type defines all variants
↓
2 Switch handles each variant explicitly
↓
3 Default branch expects never
↓
4 New variant -> never becomes non-empty -> compile error
Why this works
Why exhaustiveness matters

When you extend a union type, existing switch statements silently miss new cases. Exhaustiveness checking turns that into a hard error:

type Event = { type: "click" } | { type: "keydown" };
// Add { type: "submit" } later -> assertNever fires

This is essential for domain models where an unhandled case should never compile, not just occasionally crash.

Narrowing to never

The compiler knows a fully narrowed union leaves no possible value:

const s: Shape = ...;
if (s.kind === "circle") { /* circle */ }
else if (s.kind === "square") { /* square */ }
else {
  // here s: never
}

The final else block only runs if s is none of the union members, which is impossible. That impossibility is exactly what you assert on.

Examples

The never type

A type that cannot happen

function assertNever(x: never): never {
  throw new Error(`Unexpected: ${JSON.stringify(x)}`);
}

never represents values that cannot exist. If code reaches a never variable, something went wrong, so it is the perfect tool for exhaustive checks.

Exhaustive switch

Ensure all cases handled

type Shape = { kind: "circle"; r: number } | { kind: "square"; s: number };
function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.s ** 2;
    default:
      assertNever(s);  // error if Shape gains a new variant
      return 0;
  }
}

If a new shape variant is added, s can no longer be never, and assertNever reports a compile error, so you never forget to handle it.

Default cases and errors

Fail loudly

function handle(cmd: "start" | "stop") {
  if (cmd === "start") { /* ... */ }
  else {
    assertNever(cmd);  // cmd is never here -> compile error if union grows
  }
}

Placing assertNever in the else branch guarantees every union member is handled. It turns a runtime surprise into a guaranteed compile-time failure.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Lookup Types and Index Access
Previous Lesson
Lookup Types and Index Access
Next Lesson
Modules and Namespaces
Modules and Namespaces →