BeCuriouzBeCuriouz

Learn something new every day

🔷 TypeScript
🏷️

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.

ts
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.

ts
default:
  const _exhaustive: never = s; // errors if a case is unhandled
  return _exhaustive;