Dataslope logoDataslope

Pattern Matching

Using TypeScript's discriminated unions and exhaustiveness checking to write code that *cannot* forget a case.

In a language like Haskell, ML, Rust, or Scala, pattern matching is a first-class syntactic construct. TypeScript doesn't have a match keyword — but it has something close enough: discriminated unions + exhaustiveness checking via the never type.

Used well, this pair gives you the central guarantee of pattern matching: the compiler refuses to build code that forgets a case.

The setup: a discriminated union

A discriminated union is a sum type where every variant has a literal tag (called a discriminant) on the same property name.

Code Block
TypeScript 5.7

Inside case "circle":, the compiler narrows s to the circle variant — s.radius works; s.side doesn't. That narrowing is the heart of pattern matching in TypeScript.

The exhaustiveness trick: never

Now we add the safety net. Inside the default: branch of a switch, the value's type should be never — because we've covered every case. If a future change adds a new variant and we forget to handle it, the type stops being never and the compiler catches it.

Code Block
TypeScript 5.7

Try adding | { kind: "triangle"; base: number; height: number } to Shape (locally on your machine) and re-running the type checker — the default branch starts erroring, because s is no longer never. The compiler has told you, before you ship, that you forgot a case.

This is "compiler-assisted correctness" in its purest form. The type checker is doing the audit you'd otherwise pay a senior engineer to do in code review.

A match helper for expressions

switch is a statement. Sometimes you want pattern matching as an expression — for instance, when assigning a result. You can build a tiny helper that achieves both exhaustiveness and a nice calling syntax:

Code Block
TypeScript 5.7

The Handlers<T, R> mapped type forces you to provide a handler for every variant of T. Forget one — the compiler complains the moment you call match.

This is much closer to what pattern matching feels like in Haskell or Rust: an expression with a per-case handler, type-checked.

Nested matching

Sometimes a variant contains another variant. You can pattern match through layers by chaining match calls, or by destructuring inside a case.

Code Block
TypeScript 5.7

Recursive sum types like Json show why exhaustive matching matters: the data structure has six possible shapes, and the recursive walk must handle all of them. The compiler enforces it.

Pattern matching over multiple values

True pattern-matching languages also let you match tuples of values:

Code Block
TypeScript 5.7

For more complex tuple cases, you can build a tagged "pair" type and match on it the same way:

type Pair = readonly [Light, boolean];

…then switch on a derived discriminant such as `${current}:${emergency}`.

A visual model

Pattern matching forces you to think of code as a branching function over a finite set of shapes:

The dotted line is the safety: the moment somebody adds a new shape, the diagram literally cannot be drawn anymore — the compiler fails, and the developer is forced to extend every match.

A small challenge

Challenge
TypeScript 5.7
Implement an exhaustive event interpreter

In interpret.ts, finish apply so it handles every variant of Event using a switch with an exhaustiveness check in the default branch.

Rules:

  • { kind: "deposit", amount } → balance + amount
  • { kind: "withdraw", amount } → balance - amount
  • { kind: "interest", rate } → balance * (1 + rate), rounded to 2 decimals
  • { kind: "reset" } → 0

Expected output

100
70
73.5
0

QuestionSelect one

What does assigning the matched value to a const _: never in the default branch of a switch accomplish?

It silences ESLint warnings about empty default branches

It throws an error at runtime when an unhandled case occurs

It causes a compile error if a new variant is added to the union and not handled, because the value would no longer be of type never

It improves runtime performance of the switch

On this page