Unions, Narrowing and Discriminated Unions
IntermediateUnions model "one of these", and narrowing is how the compiler follows your checks. Discriminated unions are the single most useful pattern in the language.
Overview
A union type says a value is one of several things, and TypeScript will not let you use it until you have proved which. Narrowing is the compiler tracking your typeof, in, and equality checks and updating the type inside each branch. Once you add a shared literal field — a "tag" — you get discriminated unions, which let you model states that cannot be combined incorrectly. A request that is loading cannot also have data; expressing that in the type means the impossible combination cannot be written, and the compiler enforces that you handle every case.
Narrowing
The compiler follows control flow. Ordinary JavaScript checks are what narrow.
function format(value: string | number | null) {
if (value === null) return '—' // value: string | number after this
if (typeof value === 'string') return value.trim() // string here
return value.toFixed(2) // number — the only thing left
}
// The narrowing tools
typeof x === 'string'
x instanceof Date
'role' in user // property presence
Array.isArray(x)
x === null / x !== undefined
x?.length // optional chaining narrows too
// Truthiness narrows, and has the classic trap
if (count) { } // excludes 0 as well as undefined — usually a bug
if (count != null) { } // excludes null AND undefined, keeps 0
// A custom type guard, for when the built-ins cannot express it
function isProblem(x: unknown): x is Problem {
return typeof x === 'object' && x !== null && 'slug' in x
}
if (isProblem(data)) data.slug // narrowed by the return typeDiscriminated Unions
A shared literal field the compiler can switch on. This is how you make illegal states unrepresentable.
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: Problem[] }
| { status: 'error'; error: string }
function render(state: RequestState) {
switch (state.status) {
case 'idle': return null
case 'loading': return <Spinner />
case 'success': return <List items={state.data} /> // data exists here
case 'error': return <Error msg={state.error} /> // error exists here
}
}
// Compare with the shape people write first:
interface Bad { loading: boolean; data?: Problem[]; error?: string }
// This permits loading:true with data AND error set — a state that
// makes no sense but the compiler cannot object to.
// Result types, the same idea:
type Result<T> = { ok: true; value: T } | { ok: false; error: Error }Exhaustiveness
Make the compiler fail when someone adds a case and forgets a branch.
function label(state: RequestState): string {
switch (state.status) {
case 'idle': return 'Ready'
case 'loading': return 'Loading…'
case 'success': return 'Done'
case 'error': return 'Failed'
default: {
const _exhaustive: never = state // only compiles if nothing is left
return _exhaustive
}
}
}
// Add { status: 'cancelled' } to RequestState and this file fails to
// compile — pointing at exactly the switch that needs a new branch.
// That is the payoff: adding a state finds every place that handles states.Key Points to Remember
- 1A union value cannot be used until narrowed; typeof, in, instanceof and equality checks all narrow
- 2Truthiness checks exclude 0 and empty string too — use != null when those are valid values
- 3A custom type guard is a function returning `x is T`, for checks the built-ins cannot express
- 4A discriminated union has a shared literal tag, letting the compiler know which fields exist in each branch
- 5Assigning the value to `never` in the default branch makes forgetting a new case a compile error
Interview Questions
Sign in to ask AriaWhat is a discriminated union and what problem does it solve?
How do you make the compiler enforce that a switch handles every case of a union?
What is a user-defined type guard and when do you need one?
Ask Aria about Unions, Narrowing and Discriminated Unions
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.