Discriminated unions
Represent mutually exclusive states with a shared tag and get exhaustive narrowing from the compiler.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain discriminated unions in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Explain it simply
A discriminated union is a union whose members share a literal property such as status or type. Checking that property tells TypeScript exactly which member you have.
One-line definition: Represent mutually exclusive states with a shared tag and get exhaustive narrowing from the compiler.
02Mental model
Make impossible states unrepresentable. Instead of several optional fields that can contradict each other, create one type for each valid state and let the discriminator choose between them.
03Step by step
- List valid states.
- Give each state a unique literal tag.
- Attach only the data valid for that state.
- Narrow with switch or if.
- Use a never check to catch missing cases.
04Working example
type Result<T> = | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; message: string };
function render<T>(result: Result<T>) { switch (result.status) { case 'loading': return 'Loading'; case 'success': return result.data; case 'error': return result.message; }}data cannot exist on loading or error, and message cannot exist on success. Narrowing by status exposes only the valid payload.
05Where it is used
- Async request state
- Reducers and events
- API response variants
- Component prop variants
06Common mistakes
- Using a broad string instead of literal tags
- Making every payload property optional
- Casting instead of narrowing
- Forgetting exhaustive handling when adding a new member
07Interview answer
Show how the model prevents contradictory booleans such as isLoading and hasError both being true while data is also present.
What makes a property a useful discriminator?
Every union member has the property, and each member uses a distinct literal value for it.
DDConcept deep dives
Deep dive 1
One member represents one valid state
Independent flags such as loading, hasError, and hasData allow contradictory combinations. A discriminated union gives every valid situation one object shape, with a literal status that selects its legal payload. Invalid combinations cannot be constructed without bypassing the type system.
- Put data only on members where it is meaningful.
- Derive display booleans from the discriminator.
- Name states according to the domain rather than generic UI mechanics when possible.
Deep dive 2
Narrowing protects payload access
A switch or equality check on the discriminator lets TypeScript expose member-specific fields. Code cannot read result.data before proving success or result.message before proving failure. That moves entire classes of undefined checks and contradictory branches into compile-time feedback.
- Preserve literal values with precise annotations or as const where appropriate.
- Destructuring too early can weaken correlations in some complex patterns.
- Runtime payloads must be validated before being trusted as the union.
Deep dive 3
Reducers become explicit state machines
Pairing a state union with an event union makes transitions reviewable. Each reducer case knows both the current state's possibilities and the action payload. An exhaustive render handles every state, while transition tests document which events are accepted from each state.
- Not every invalid transition must be a type error; reducers can ignore or report impossible runtime events.
- Split orthogonal concerns instead of multiplying every combination into one enormous union.
- Use never checks to surface new states and events during maintenance.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Coding · 1 min · Question 1What is a discriminated union?Open model answer
Model answer
It is a union whose members share a property with distinct literal values. Checking that discriminator narrows the value to one member, exposing only the fields valid for that state.
type Result = | { status: 'ok'; data: string } | { status: 'error'; message: string };
function handle(r: Result) { if (r.status === 'ok') return r.data; // 'message' not available return r.message; // 'data' not available}Intermediate · Coding · 1 min · Question 2How does it make impossible states unrepresentable?Open model answer
Model answer
Instead of independent booleans and optional fields that can contradict each other, define one member for each valid state with only its valid payload. The type system then rejects combinations the domain does not allow.
// Representable but impossible: loading + error + data all setinterface Bad { loading: boolean; error?: string; data?: Data }
type Good = | { status: 'loading' } | { status: 'error'; error: string } | { status: 'loaded'; data: Data };Intermediate · Conceptual · 1 min · Question 3What makes a good discriminator?Open model answer
Model answer
It exists on every member, uses a distinct stable literal in each one, and describes domain identity such as status, kind, or type. A broad string cannot support exhaustive narrowing.
Open question page →Advanced · Coding · 1 min · Question 4How do you enforce exhaustive handling?Open model answer
Model answer
Use a switch on the discriminator and pass the default value to an assertNever function or assign it to never. Adding a new union member then creates a compile error at every non-exhaustive branch.
function assertNever(x: never): never { throw new Error('Unhandled: ' + JSON.stringify(x));}
switch (result.status) { case 'ok': return result.data; case 'error': return result.message; default: return assertNever(result); // compile error if a case is missing}Intermediate · Conceptual · 1 min · Question 5Can discriminated unions model API responses?Open model answer
Model answer
Yes, but untrusted JSON still needs runtime validation before it can be treated as the union. After validation, the discriminator provides safe control flow for success, domain error, and other response variants.
Open question page →Intermediate · Conceptual · 1 min · Question 6When can a large union become difficult to maintain?Open model answer
Model answer
If unrelated concerns are multiplied into one union, combinations explode. Separate orthogonal state, nest smaller state machines, or model shared data outside the union while retaining genuine mutual exclusions.
Open question page →Intermediate · Conceptual · 1 min · Question 7Can a boolean be a discriminator?Open model answer
Model answer
A true or false literal can discriminate two members, but descriptive string literals usually scale and communicate domain meaning better as more states appear.
Open question page →Advanced · Conceptual · 1 min · Question 8How do optional discriminators weaken a union?Open model answer
Model answer
If a tag can be missing, branches overlap and exhaustive reasoning becomes harder. Require a distinct literal on every member whenever the domain supports it.
Open question page →Advanced · Conceptual · 1 min · Question 9How should a default server response be handled?Open model answer
Model answer
Validate the unknown response and explicitly reject unsupported discriminator values. A compile-time exhaustive switch cannot protect against malformed or newer unvalidated payloads.
Open question page →Advanced · Conceptual · 1 min · Question 10What is a nested state machine?Open model answer
Model answer
It composes smaller discriminated states for concerns with different transitions instead of multiplying every combination into one unwieldy top-level union.
Open question page →SCScenario questions
Scenario 1
Model a media upload that may be idle, uploading with progress, processing, complete with URL, or failed with retryability.
- List mutually exclusive states and their valid data.
- Give each a stable status literal.
- Attach progress only to uploading and URL only to complete.
- Define events and exhaustively render every state.
Reveal worked answer
The union contains idle, uploading {progress}, processing, complete {url}, and failed {message, retryable}. Rendering switches on status, so accessing url while uploading is a compile error. A reducer defines legal transitions and prevents progress updates after completion.