What are Discriminated Unions in TypeScript?
Learn TypeScript discriminated unions: how the discriminant narrows types, exhaustiveness checks with never, and a clean code example for safer variants.
Expected Interview Answer
A discriminated union is a union of object types that all share a common literal property — the discriminant — which TypeScript uses to narrow the union to one exact member.
Each member type carries a singleton literal field (often called kind, type, or tag) with a unique value. When you check that field in a switch or if statement, the compiler narrows the union so the other properties of the matched member become safely accessible. Combined with an exhaustiveness check using the never type, discriminated unions let the compiler prove every case is handled.
- Type-safe narrowing without casts
- Compiler-enforced exhaustiveness with never
- Self-documenting shape of each variant
- Catches unhandled cases at compile time
- Models state machines and events cleanly
AI Mentor Explanation
A discriminated union is like a delivery's outcome recorded with a single result tag: the scorer writes 'wicket', 'runs', or 'wide'. Once you read that tag you know exactly which extra fields to expect — dismissal type for a wicket, run count for runs — so nobody looks for a batsman's dismissal on a plain single.
Step-by-Step Explanation
Step 1
Add a discriminant
Give every member type a shared literal property such as kind with a unique string value.
Step 2
Form the union
Combine the member types with the | operator into a single union type.
Step 3
Switch on the discriminant
Use switch or if on the discriminant field so TypeScript narrows to one member.
Step 4
Access narrowed fields
Inside each branch, the member-specific properties are safely typed and accessible.
Step 5
Enforce exhaustiveness
Add a default branch assigning the value to a never variable so unhandled cases fail to compile.
What Interviewer Expects
- Definition of the discriminant property
- How narrowing works in switch/if branches
- Exhaustiveness checking with the never type
- When to prefer unions over inheritance
- A correct code example with a shared literal tag
Common Mistakes
- Using a non-literal type (string) for the discriminant so narrowing fails
- Forgetting the shared property, leaving members structurally indistinguishable
- Skipping the never exhaustiveness check
- Casting with as instead of letting the compiler narrow
Best Answer (HR Friendly)
“A discriminated union is a way to describe a value that can be one of several shapes, where each shape carries a small label. TypeScript reads that label and then knows exactly which other fields are safe to use, which prevents whole classes of bugs.”
Code Example
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'rectangle'; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'rectangle':
return shape.width * shape.height;
default: {
const _exhaustive: never = shape;
return _exhaustive;
}
}
}Follow-up Questions
- How does the never type enforce exhaustiveness?
- What makes a property a valid discriminant?
- How do discriminated unions compare to class inheritance?
- Can a union have more than one discriminant property?
- How does narrowing behave with nested discriminated unions?
MCQ Practice
1. What property type makes a discriminated union narrowable?
The discriminant must be a singleton literal (like 'circle') so each member is distinguishable and TypeScript can narrow on its value.
2. How do you get compile-time exhaustiveness checking?
Assigning the narrowed value to a never-typed variable fails to compile if any case is left unhandled.
3. Which statement best narrows a discriminated union?
Switching on the shared literal discriminant lets TypeScript narrow the union to a single member in each case.
Flash Cards
What is a discriminant? — A shared property with a unique literal value that identifies each member of a union.
How is exhaustiveness enforced? — Assign the narrowed value to a never variable in the default branch; unhandled cases break the build.
Why literal, not string? — Only singleton literals let the compiler distinguish members and narrow reliably.
Common use cases? — State machines, Redux-style actions, event handling, and API result variants.