Algebraic Data Types
Modeling possibility with sums and products — the type-level vocabulary that makes functional design precise.
When you sit down to model a domain (an order, a user, a request, a payment), you are choosing a shape. The shape determines which states are possible, which are impossible, and which mistakes the compiler can catch for you.
Algebraic data types (ADTs) are the type-level vocabulary functional programmers use to build those shapes. There are exactly two atomic moves — and and or — and you combine them to express anything you need.
Two operations: and (products) and or (sums)
- Product = "A and B". A pair. A record. A tuple.
A
Userhas anameand anage. - Sum = "A or B". A choice. A union with a tag.
A
Shapeis aCircleor aRectangleor aTriangle.
That's the whole algebra. Every data model is some combination of these two operations.
Why "algebraic"?
If you count the values a type can have, products multiply and sums
add. A boolean has 2 values; a pair of booleans has 2 × 2 = 4.
A choice between a boolean and a string has 2 + (all strings).
The cardinality literally follows the rules of arithmetic — hence
algebraic data types.
Products in TypeScript: records and tuples
A product type is "all of these fields, simultaneously". TypeScript already has them — they're records and tuples. There's nothing novel here.
Sums in TypeScript: discriminated unions
The interesting half. A sum says "one of these alternatives". In TypeScript, we model it as a discriminated union — a union of record types, each with a unique tag field.
A few things to notice:
- The compiler narrows
sto a specific variant inside eachcase. You can accesss.radiusonly after confirmings.kind === "circle". There is no need for type assertions oras Circle. - If you add a new variant (
square) and forget to handle it, the compiler will warn — provided you use thenevertrick (next section).
Exhaustiveness checking with never
Try adding a { kind: "triangle"; base: number; height: number }
variant to Shape and see how the compiler reacts. This is the
trick that turns "I think this switch covers everything" into "the
compiler will prove it does".
Modeling possibility precisely
The most common beginner mistake is to model alternatives with optional fields and booleans. That works, but it allows nonsense states to exist in the type. Sums let you forbid them.
Bad: "any of these fields might be set"
The type allows "active and suspended" — a nonsense state. Every
function that takes BadUser now has to check at runtime: is this
really suspended?
Good: a sum that forbids nonsense
Now "active and suspended" is unrepresentable. The data carried by
each variant (reason, until, deletedAt) is tied to the case
that needs it — you can never have suspendedUntil without also
being suspended.
This is the slogan "make illegal states unrepresentable" in action.
A small zoo of useful sums
You'll use these shapes constantly. We define each here; later chapters explore them in depth.
Option: a value or nothing
Replaces null and undefined with a type the compiler tracks.
Result: success or failure
Replaces exceptions with a type that forces the caller to handle the error case.
Tree: recursive sums
A recursive sum: a tree is either empty, or a node holding a value
and two subtrees. Three lines of definition; the entire algorithm
(walking it, transforming it, folding it) follows from switch.
Designing with sums and products together
Real domain types interleave the two operations.
In TypeScript:
Sums nest inside sums (Event.payment_received.method is itself a
sum). Records carry the fields appropriate to each variant. The
compiler tracks both layers automatically.
A multi-file challenge
In shape.ts, define a discriminated union Shape
that has four variants:
circle—radiusrectangle—width,heighttriangle—base,heightsquare—side
Implement area(s: Shape): number. Use a switch and the
never exhaustiveness pattern in the default case.
In main.ts, the test data is shipped; do not modify it.
Expected output
circle: 12.566370614359172
rectangle: 12
triangle: 15
square: 16
Why is a discriminated union ("sum type") often a better model than a record with several optional fields?
Unions compile faster
Unions use less memory at runtime
A discriminated union ties each piece of data to the variant that needs it, so impossible combinations of fields cannot exist
Unions don't need a "kind" tag