Algebraic Data Types

How do you build the data your program works with? Functional languages give you two beautifully simple ways to combine types, and everything else is made from them. Because these two combinators behave like multiplication and addition, the types you build with them are called algebraic data types (ADTs).

"And" and "or" — that's the whole toolkit. From it you can model essentially any data, and the compiler will help you handle every case.

Why "product" and "sum"? Count the possibilities

The names aren't a metaphor — they're arithmetic on the number of possible values. Think of each type as the set of values it can take.

|A \times B| = |A|\cdot|B| \qquad\qquad |A + B| = |A| + |B|

This little algebra is genuinely useful: it tells you how many states your data can be in, and therefore how many cases you must handle.

Modelling a sum type in TypeScript

TypeScript spells a sum type as a tagged (discriminated) union: each alternative is a record carrying a kind tag that says which one it is. Here is a Shape that is either a circle or a rectangle — a sum of two product types.

type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number }; const c: Shape = { kind: "circle", radius: 5 }; const r: Shape = { kind: "rect", width: 3, height: 4 }; console.log(c.kind, c.radius); console.log(r.kind, r.width, r.height);

Each alternative is a product (a circle is a tag and a radius); the union of them is a sum. This is the shape of almost every "one of several possibilities" type you'll model — a result that's Ok or Error, a tree node that's a Leaf or a Branch, an event that's a Click or a Keypress.

Pattern matching: handle every case

A sum type is only half the story; the other half is pattern matching — branching on which alternative you have and pulling out its fields. In TypeScript you switch on the tag, and the type-checker narrows the type inside each branch, so shape.radius is available only in the circle case.

type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number }; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; // s.radius known here case "rect": return s.width * s.height; // s.width/height known here } } console.log("circle area:", area({ kind: "circle", radius: 5 }).toFixed(2)); console.log("rect area: ", area({ kind: "rect", width: 3, height: 4 }));

The pairing is the point: sum types + pattern matching together let you say "there are exactly these cases, and here's what to do for each," with the compiler checking you didn't forget one.

The quiet superpower of ADTs is designing so bad data can't exist. Suppose a network request is either loading, or succeeded with data, or failed with an error. A tempting product design bolts three optional fields onto one record: { loading, data?, error? } — which allows nonsense like "loading and failed", or "succeeded but data is missing." A sum type forbids all of that:

type Request = | { kind: "loading" } | { kind: "success"; data: string } | { kind: "failure"; error: string }; function describe(r: Request): string { switch (r.kind) { case "loading": return "Please wait..."; case "success": return "Got: " + r.data; // data guaranteed present case "failure": return "Error: " + r.error; // error guaranteed present } } console.log(describe({ kind: "loading" })); console.log(describe({ kind: "success", data: "hello" })); console.log(describe({ kind: "failure", error: "timeout" }));

In the success case data must be there; in the failure case error must be there — and "loading and failed at once" simply cannot be written. Whole classes of bug vanish at the design stage. This is why functional programmers reach for sum types so eagerly.

When you pattern-match on a sum type, handle every case. A switch that quietly falls through on an unhandled tag returns undefined and slips past you. Two safeguards:

This is a real advantage over a bag of booleans: add a new case to a sum type and the type-checker hunts down every match that must change.