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).
x and a y."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.
The names aren't a metaphor — they're arithmetic on the number of possible values.
Think of each type as the
(Bool, Bool) pair has Bool or a Bool" has
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.
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.
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.
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.
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:
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:
: number) — then
a missing branch that leaves a path returning nothing becomes a type error, not a
runtime surprise.never. If you later add a new alternative and forget to handle it, that line
stops compiling — the compiler marches you to every place that needs updating.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.