Discriminated Unions
A discriminated union is a union of object types that all share one property holding a unique literal value — the discriminant — so that checking that single field lets TypeScript narrow the value to exactly one member and safely access the fields that member carries.
Learn Discriminated Unions in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of mail arriving at a sorting office. Every envelope has a class stamp in the corner — "Letter", "Parcel", "Registered". That stamp is the discriminant. The clerk reads the stamp first, and only then knows what to expect inside: a parcel has a weight, a registered item has a tracking number, a plain letter has neither. You'd never weigh a letter or look for a tracking number on a postcard. A discriminated union works the same way — read the tag, and the compiler reveals exactly which fields are valid for that kind of "envelope".
1. The Discriminant & Narrowing with switch
Give every member of the union the same field — here kind — set to a distinct literal. When you switch on that field, TypeScript narrows the value inside each case to the single member that matches, so only that member's fields are accessible:
2. Exhaustiveness Checks with never
A discriminated union shines when you make the compiler prove you handled every case. In the default branch, assign the value to never (or pass it to an assertNever helper). If a case is missing, the value isn't never and the build fails — a forgotten variant becomes a compile error:
3. Modelling State & Results
The pattern's real payoff is modelling state machines so that impossible states are unrepresentable . A request can be idle , loading , success (with data), or error (with a message) — and a reducer switches on the discriminant to move between them. You can't accidentally read data while loading :
🎯 Your Turn
Complete a function that narrows a Payment union by its method discriminant. Fill in the blanks and match the expected output.
Build an unwrapResult for a boolean-discriminated Result union — return the value on success, the fallback on failure. Follow the outline, run it, and match the example output.
Practice quiz
What makes a union 'discriminated'?
- It has more than three members
- All members are objects
- Every member shares a property set to a unique literal type (the discriminant)
- It uses the never type
Answer: Every member shares a property set to a unique literal type (the discriminant). A shared property with a unique literal value per member (kind/type/status) is the discriminant.
Common names for the discriminant field include:
- kind, type, status
- id, key, ref
- name, label, tag only
- value, data, payload
Answer: kind, type, status. Conventional discriminant field names are kind, type, or status.
When you switch on the discriminant, what does TypeScript do inside each case?
- Nothing — narrowing is runtime-only
- Widens it to the full union
- Casts it to any
- Narrows the value to the single member whose discriminant matches
Answer: Narrows the value to the single member whose discriminant matches. Control-flow analysis narrows the value to exactly the matching member in each case.
Inside 'case "circle":' for a shape union, which field is accessible?
- width
- radius
- size
- all of them
Answer: radius. After narrowing to the circle member, only its own fields (like radius) are available.
What is the purpose of the never-based exhaustiveness check?
- To make the compiler error if a new variant is added but a case is forgotten
- To speed up the switch
- To throw at runtime always
- To widen the union
Answer: To make the compiler error if a new variant is added but a case is forgotten. Assigning the value to never (or assertNever) turns a forgotten case into a compile error.
What signature does the exhaustiveness helper typically have?
- function assertNever(x: any): void
- function assertNever(x: unknown): boolean
- function assertNever(x: never): never
- function assertNever(): void
Answer: function assertNever(x: never): never. assertNever(x: never): never errors at compile time if x is not actually never.
Why must the discriminant be a literal type, not a wide 'string'?
- Strings are slower
- Widening to string stops narrowing from working
- Literals use less memory
- string is not allowed in unions
Answer: Widening to string stops narrowing from working. If the discriminant is widened to string, TypeScript can no longer narrow per case.
Which is a textbook use of a discriminated union?
- A single number
- A plain string array
- A boolean flag alone
- Request state: idle | loading | success | error
Answer: Request state: idle | loading | success | error. Modelling request state (idle/loading/success/error) is a classic discriminated-union use.
What is the idiomatic Result type discriminated by a boolean?
- { value } | { error }
- { ok: true; value } | { ok: false; error }
- { status: number }
- { data?: T }
Answer: { ok: true; value } | { ok: false; error }. A boolean 'ok' field discriminates the success ({ ok: true; value }) and failure ({ ok: false; error }) cases.
What goes wrong if two members share the same discriminant value?
- Nothing
- The compiler renames one
- They collapse together and narrowing breaks
- It becomes a tuple
Answer: They collapse together and narrowing breaks. Each member needs a UNIQUE discriminant value; duplicates break narrowing.