BeCuriouzLearn something new every day
Discriminated Unions: One Tag to Rule Them All
💡 A shared literal 'tag' field lets TypeScript narrow a union member-by-member and, via a never default, force you to handle every case.
Recall from earlier cards how TypeScript narrows a union type when you check a property. A discriminated union (also called a tagged union) supercharges this: every member of the union shares one common property — the discriminant (or tag) — whose value is a unique literal type. Checking that one field tells the compiler exactly which member you're holding.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2; // s is the circle member
case "square": return s.side ** 2;
case "rect": return s.w * s.h;
}
}Here kind is the discriminant. Inside case "circle", TypeScript knows s.radius exists but s.side does not — no manual type guard needed. This is cleaner and safer than checking for the presence of individual fields.
The real payoff is exhaustiveness checking. Assign the switch's default branch to a variable of type never — the type inhabited by nothing. If every case is handled, the remaining type is never and it compiles. But add a new member like { kind: "triangle" } and forget a case, and s is no longer never, so the assignment becomes a compile-time error that points you straight at the missing branch.
default:
const _exhaustive: never = s; // errors if a case is unhandled
return _exhaustive;