Skip to main content

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

javascript
type Circle = { kind: "circle"; radius: number; }; type Square = { kind: "square"; side: number; }; type Shape = Circle | Square;

Here:

  • Shape is a union of two types (Circle | Square),
  • both have a shared property, kind,
  • the value of kind is 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.

javascript
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:

  1. Finds the property common to all members of the union (kind);
  2. Checks that it takes unique literal values ("circle", "square");
  3. 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

javascript
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 as casts and manual checks;
  • guarantee exhaustive handling of all cases.

Real-world examples

1. Loading states (state machine pattern)

javascript
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

javascript
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

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

javascript
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 unionDiscriminated union
`type A = {a: number}{b: string}`
No common markerThere is a common tag field (kind)
TS does not know which variant is activeTS can narrow the type automatically
Requires manual checks ('a' in obj)A check on kind or switch is enough

Summary

TermMeaning
Discriminated unionA union of types with a common discriminant
DiscriminantA common property with unique literal values
Why it's neededLets TypeScript automatically narrow the type and check all cases
Typical useState machines, Redux actions, API responses, error handling
Main advantageSafe 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.