What are Discriminated Unions?
What a Discriminated Union is
A Discriminated Union is a union (union) of several object types
that share a common tag property (a discriminant),
by which TypeScript can unambiguously determine exactly which type it is dealing with.
Example
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
side: number;
};
type Shape = Circle | Square;Here:
Shapeis a union of two types (Circle | Square),- both have a shared property,
kind, - the value of
kindis unique to each type.
This is the discriminant field.
Why a "discriminant" is needed
TypeScript can narrow the type (narrowing) based on the value of the kind property.
function area(shape: Shape) {
if (shape.kind === "circle") {
// Here shape: Circle
return Math.PI * shape.radius ** 2;
} else {
// Here shape: Square
return shape.side ** 2;
}
}TypeScript sees shape.kind === "circle"
and automatically determines that shape is now Circle.
How this works under the hood
TypeScript does the following:
- Finds the property common to all members of the union (
kind); - Checks that it takes unique literal values (
"circle","square"); - When you check
shape.kind === "circle", TS excludes all other variants (Square).
This is discriminated union narrowing - automatic type narrowing based on the discriminant.
An example with several variants
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}In the switch block TypeScript itself narrows the type of shape at each step.
And never in default guarantees that all cases are handled.
Why this is convenient
Discriminated unions let you:
- build safe enum-like constructs;
- type application states, API results, Redux actions;
- avoid unnecessary
ascasts and manual checks; - guarantee exhaustive handling of all cases.
Real-world examples
1. Loading states (state machine pattern)
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string };
type ErrorState = { status: "error"; message: string };
type RequestState = LoadingState | SuccessState | ErrorState;
function render(state: RequestState) {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Data: ${state.data}`;
case "error":
return `Error: ${state.message}`;
}
}2. Redux-like actions
type Action =
| { type: "add"; payload: number }
| { type: "remove"; id: string }
| { type: "reset" };
function reducer(state: number[], action: Action) {
switch (action.type) {
case "add":
return [...state, action.payload];
case "remove":
return state.filter((_, i) => i.toString() !== action.id);
case "reset":
return [];
}
}3. API responses
type ApiResponse<T> =
| { kind: "ok"; data: T }
| { kind: "error"; message: string };
function handleResponse(res: ApiResponse<string>) {
if (res.kind === "ok") {
console.log("OK:", res.data);
} else {
console.error("Error:", res.message);
}
}An exhaustiveness check (never exhaustiveness check)
A useful technique: add a check that all variants are handled.
function checkExhaustive(value: never): never {
throw new Error("Unhandled case: " + value);
}
function draw(shape: Shape) {
switch (shape.kind) {
case "circle":
return "Drawing circle";
case "square":
return "Drawing square";
default:
return checkExhaustive(shape); // if a new variant appears, TS will produce an error
}
}Discriminated unions vs. regular unions
| Regular union | Discriminated union |
|---|---|
| `type A = {a: number} | {b: string}` |
| No common marker | There is a common tag field (kind) |
| TS does not know which variant is active | TS can narrow the type automatically |
Requires manual checks ('a' in obj) | A check on kind or switch is enough |
Summary
| Term | Meaning |
|---|---|
| Discriminated union | A union of types with a common discriminant |
| Discriminant | A common property with unique literal values |
| Why it's needed | Lets TypeScript automatically narrow the type and check all cases |
| Typical use | State machines, Redux actions, API responses, error handling |
| Main advantage | Safe branching without as, with full typing and hints |
Just remember:
A Discriminated Union is when different shapes of data have a "passport" (a discriminant), and TypeScript uses that passport to know exactly what it is looking at.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.