TypeScriptUnions and narrowing
Discriminated unions
Unions of objects with a shared tag field that makes it easy to tell the variants apart.
Example
#TypeScript
type RequestState =
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; message: string }
function render(state: RequestState): string {
switch (state.status) {
case 'loading':
return 'Loading…'
case 'success':
return `Courses: ${state.data.join(', ')}`
case 'error':
return `Error: ${state.message}`
}
}
console.log(render({ status: 'loading' }))
console.log(render({ status: 'success', data: ['HTML', 'CSS'] }))
console.log(render({ status: 'error', message: 'No connection' }))Result
Loading… Courses: HTML, CSS Error: No connection
Definition and usage
#A discriminated union is a union of object types where every variant has a shared field with a different literal value, for example type: "text" | "image". That field acts as a tag.
After checking the tag (if (block.type === "image"), or a switch) the compiler knows exactly which variant it’s dealing with, and gives you access to its fields.
This is the basic pattern for modeling states (loading, success, error), events, messages, or task types. Combined with never in the default branch, it gives you exhaustiveness checking.
Syntax
#Syntax
type A = { kind: "a"; … }
type B = { kind: "b"; … }
type Union = A | BGood practices
#- Model state as a discriminated union instead of several loose flags (
isLoading,error,data). Impossible combinations stop being possible. - Add a
defaultbranch withassertNever, so the compiler warns you about unhandled variants.
Related entries
#- Union (A | B)A type that can be one of several types.
- neverThe type of a value that never occurs: functions that don’t end normally, and impossible cases.
- Type narrowingDetermining a more precise type for a value based on checks in your code.
- Literal typesTypes that allow only specific values, e.g. "easy" | "hard".
Spotted a mistake or missing an example? Write to us.