Learn TypeScript discriminated unions and exhaustive pattern matching with runnable examples. Result types, state machines, and safe switch-case.
A discriminated union (also called a tagged union) is a union type where each member has a common literal field — the 'discriminant' or 'tag' — that lets TypeScript figure out which variant you're holding. Inside a switch on that tag, TypeScript narrows to each specific variant, including its extra fields. Discriminated unions are the safest way to model 'one of N shapes' — API responses, Redux actions, state machines, Result<T> / Either types.
type Result<T> = { ok: true; value: T } | { ok: false; error: string }. The tag here is `ok`, a literal true or false. Any distinct set of literals works: 'loading'|'success'|'error', 'add'|'remove'|'update', etc.
if (result.ok) narrows to the ok: true variant, so result.value is accessible. else narrows to ok: false and result.error. A switch on the tag field gives you the same narrowing in each case.
Assign the narrowed value to a variable of type never in the default case. If you ever add a new variant without handling it, the never assignment fails to compile — compiler-enforced exhaustiveness.
Because the tag is a value in the object itself, narrowing is a plain property check at runtime. No class hierarchy, no instanceof, no reflection.
Any time you're modelling a fixed set of shapes where each shape carries different data: parsed AST nodes, Redux/Flux actions, API response types, loading/success/error state, Result<T, E>, network protocol messages. If you find yourself writing `data: any` because the shape varies, reach for a discriminated union instead.
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
function divide(a: number, b: number): Result<number> {
if (b === 0) return { ok: false, error: "Division by zero" };
return { ok: true, value: a / b };
}
function display(result: Result<number>) {
if (result.ok) {
console.log("Result:", result.value);
} else {
console.error("Error:", result.error);
}
}
display(divide(10, 3));
display(divide(10, 0));
// State machine with exhaustiveness
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function render(state: State): string {
switch (state.status) {
case "idle": return "Press start";
case "loading": return "Loading...";
case "success": return `Got ${state.data.length} items`;
case "error": return `Oops: ${state.message}`;
default: {
const _exhaustive: never = state;
return _exhaustive;
}
}
}
console.log(render({ status: "loading" }));
console.log(render({ status: "success", data: ["a", "b", "c"] }));
Open this example in the TryJS playground to edit and run the code instantly in your browser — no signup needed.
A union type where each member has a common literal field (the discriminant) that TypeScript uses to narrow between the variants. It's the type-safe way to model 'one of N shapes' without runtime class hierarchies.
Add a default case that assigns the narrowed value to a variable of type never. const _exhaustive: never = value. If you add a new variant and forget to handle it, the assignment fails to compile.
Yes — literal booleans (Result's ok: true/false), numbers, and even specific object shapes work. String literals are the most common because they're readable and hard to mistype.
Conceptually, yes. Haskell's data types, Rust's enums with variants, and OCaml's variants are all sum types. Discriminated unions are TypeScript's way to express the same idea with structural typing and literal tags.