Chapter 2 · Practice 2 of 6
Model states as a discriminated union, not a bag of optional fields
Why it matters
A type like { loading: boolean; data?: User; error?: string } allows states that make no sense: loading with an error, or neither data nor error after loading. Every consumer has to guess which combinations are real.
A discriminated union lists each real state with a shared literal field, usually status or kind. The narrowing chapter of the handbook shows how checking that field narrows the whole object, so inside if (state.status === 'success') TypeScript knows state.data exists.
This pattern fits:
- request state (idle, loading, success, error),
- events and messages,
- payment or order lifecycles,
- results that can fail.
Do
- Give every variant a literal
statusorkindfield - Put fields on the variant where they exist, such as
dataonly on success
Avoid
- Several booleans and optional fields that must be kept in sync by hand
Example
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function label(state: RequestState<User>): string {
if (state.status === 'success') return state.data.name; // data exists here
if (state.status === 'error') return state.error;
return 'Loading';
}