Narrowing and Discriminated Unions in TypeScript
A union type says "this value is one of several shapes". Narrowing is how TypeScript figures out which one you have inside a branch.
Built-in narrowing
function format(value: string | number | Date) {
if (typeof value === 'string') return value.trim(); // string
if (value instanceof Date) return value.toISOString(); // Date
return value.toFixed(2); // number
}
Discriminated unions
Give every variant a common literal field (the discriminant). This models states far better than a bag of optional fields:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function render(state: RequestState<User[]>) {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return state.data.map((u) => u.name).join(', '); // data exists here
case 'error':
return `Failed: ${state.error}`; // error exists here
case 'idle':
return '';
}
}
Impossible states, like "loading with an error", can't even be written.
Exhaustiveness checking
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
// In the switch above:
default:
return assertNever(state); // compile error if a new status is added and not handled
Custom type guards
interface Cat { meow(): void }
interface Dog { bark(): void }
function isCat(pet: Cat | Dog): pet is Cat {
return 'meow' in pet;
}
if (isCat(pet)) pet.meow();
Type guards are only as correct as their implementation. For external data, a schema validator is safer than a hand-written guard.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.