Difficulty: Intermediate
What are discriminated unions? How do you implement them and ensure exhaustive handling?
Discriminated unions (also called tagged unions or algebraic data types) are union types where each member has a common property with a unique literal value. This 'discriminant' property lets TypeScript narrow the type in conditional branches.
Three requirements: 1. A common property present in all members (the 'tag' or 'discriminant') 2. Each member has a unique literal type for that property 3. A type alias that unions all members
Discriminated unions are one of TypeScript's most powerful patterns. They model state machines, API responses, events, and any scenario with distinct variants. Combined with exhaustive checking, they ensure you handle every case.
// Result type (like Rust's Result)
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { ok: false, error: 'Division by zero' };
}
return { ok: true, value: a / b };
}
const result = divide(10, 3);
if (result.ok) {
console.log(result.value.toFixed(2)); // OK: narrowed to { ok: true; value: number }
} else {
console.log(result.error.toUpperCase()); // OK: narrowed to { ok: false; error: string }
}
// Loading state pattern
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function renderUser(state: AsyncState<User>): string {
switch (state.status) {
case 'idle': return 'Click to load';
case 'loading': return 'Loading...';
case 'success': return `Hello, ${state.data.name}`;
case 'error': return `Error: ${state.error}`;
}
}
The discriminant property ('ok' or 'status') lets TypeScript narrow to the exact variant in each branch. Properties unique to a variant are only accessible after narrowing.
type PaymentMethod =
| { type: 'credit-card'; cardNumber: string; expiry: string; cvv: string }
| { type: 'paypal'; email: string }
| { type: 'bank-transfer'; accountNumber: string; routingNumber: string };
// Helper: ensures all cases are handled
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
}
function processPayment(method: PaymentMethod): string {
switch (method.type) {
case 'credit-card':
return `Charging card ending in ${method.cardNumber.slice(-4)}`;
case 'paypal':
return `Redirecting to PayPal for ${method.email}`;
case 'bank-transfer':
return `Initiating transfer from account ${method.accountNumber}`;
default:
return assertNever(method); // Compile error if a case is missing
}
}
// If you add 'crypto' to PaymentMethod and forget to handle it:
// Error: Argument of type '{ type: "crypto"; ... }' is not
// assignable to parameter of type 'never'
The assertNever helper catches unhandled variants at compile time. When you add a new variant, TypeScript immediately tells you every switch that needs updating.
// Redux-style actions with discriminated unions
type Action =
| { type: 'ADD_TODO'; payload: { text: string } }
| { type: 'TOGGLE_TODO'; payload: { id: number } }
| { type: 'DELETE_TODO'; payload: { id: number } }
| { type: 'CLEAR_COMPLETED' };
interface Todo {
id: number;
text: string;
completed: boolean;
}
function todoReducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case 'ADD_TODO':
return [...state, {
id: Date.now(),
text: action.payload.text, // TS knows payload has 'text'
completed: false,
}];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload.id // TS knows payload has 'id'
? { ...todo, completed: !todo.completed }
: todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload.id);
case 'CLEAR_COMPLETED':
// No payload here - TS knows this variant has no payload
return state.filter(todo => !todo.completed);
}
}
Discriminated unions are the backbone of type-safe state management. Each action variant carries exactly the data it needs, and TypeScript ensures correct access.
Discriminated Unions, Tagged Unions, Pattern Matching, Exhaustive Checks, never