Union types and narrowing
Unions, literal types, typeof/in/instanceof, discriminated unions and never.
Updated
What a union type is
A union says: the value can be one of several types.
type Id = string | number
type Status = 'idle' | 'loading' | 'success' | 'error'It's one of the most used tools in TS: statuses, component variants, responses that can be a success or an error.
What narrowing is
If a value is string | number, TS only lets you use what both have in common. To use string methods, you have to prove it's a string. That's narrowing: an if after which TS knows the exact type.
function format(id: string | number) {
id.toUpperCase() // ✗ number has no toUpperCase
if (typeof id === 'string') {
return id.toUpperCase() // ✓ here TS knows: string
}
return id.toFixed(0) // ✓ here: number
}Kinds of narrowing — and when
| Check | For | Example |
|---|---|---|
typeof x === 'string' |
primitives | string | number |
x === null, x !== undefined, if (x) |
missing values | User | null |
'key' in x |
objects with different shapes | Cat | Dog |
x instanceof Error |
classes | catch (e) |
Array.isArray(x) |
an array vs something else | string | string[] |
x.kind === 'circle' |
discriminated unions | see below |
an x is T function |
a reusable check | isUser(x) |
Discriminated unions — the most useful pattern
Each variant has a common field with a different literal value (the discriminant). A switch on it narrows automatically:
type Result =
| { status: 'success'; data: User }
| { status: 'error'; message: string }
| { status: 'loading' }
function render(r: Result) {
switch (r.status) {
case 'success': return r.data.name // TS knows data exists
case 'error': return r.message // here, message
case 'loading': return 'Loading…'
}
}It's impossible to read data in an error state — a whole class of bugs disappears. The same pattern describes a reducer's actions in React.
Exhaustive checks with never
If you later add { status: 'empty' }, you want TS to show you every switch that doesn't handle it:
default: {
const unreachable: never = r // ✗ an error if a variant is left unhandled
return unreachable
}Type predicates
A function that checks a type and "tells" TS the result:
function isString(x: unknown): x is string {
return typeof x === 'string'
}
const values: unknown[] = ['a', 1, 'b']
const strings = values.filter(isString) // string[]Summary
- A union = "one of"; literal unions for statuses and variants.
- Narrowing =
typeof,in,instanceof, equality — after theif, TS knows the type. - Discriminated unions +
switch+never= code without impossible states.