Discriminated unions
Discriminated union — union объектов с общим полем-дискриминатором. Мощный паттерн для представления состояний и вариантов.
Пример
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'rectangle'; width: number; height: number };
Поле kind — общее для всех вариантов, но с разными значениями. Это дискриминатор.
Использование
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'rectangle':
return shape.width * shape.height;
}
}
В каждом case TypeScript знает точный вариант и даёт доступ к его полям.
Практика: состояния запроса
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function render(state: RequestState<User>) {
switch (state.status) {
case 'idle':
return <p>Начните поиск</p>;
case 'loading':
return <Spinner />;
case 'success':
return <UserCard user={state.data} />;
case 'error':
return <Error message={state.error.message} />;
}
}
Невозможные состояния не выразить: нельзя быть loading и success одновременно.
Exhaustive check
never проверяет, что все варианты обработаны:
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
case 'rectangle': return shape.width * shape.height;
default:
const _exhaustive: never = shape;
throw new Error('Unknown shape');
}
}
Если добавить новый вариант в Shape и забыть обработать — TypeScript выдаст ошибку на never.
С дискриминатором-числом
type Event =
| { type: 1; payload: string }
| { type: 2; payload: number };
Обычно используют строки — читаемее.
С вложенными
type Notification =
| { kind: 'email'; address: string; subject: string }
| { kind: 'sms'; phone: string; text: string }
| { kind: 'push'; deviceId: string; title: string; body: string };
В React
type Props =
| { variant: 'text'; children: string }
| { variant: 'icon'; icon: React.ReactNode }
| { variant: 'link'; href: string; children: string };
function Button(props: Props) {
if (props.variant === 'text') {
return <button>{props.children}</button>;
}
if (props.variant === 'icon') {
return <button>{props.icon}</button>;
}
return <a href={props.href}>{props.children}</a>;
}
Reducer
Discriminated unions идеально подходят для редьюсеров:
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; value: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'set': return action.value;
}
}
Плюсы
- Невозможные состояния не компилируются
- Все варианты обработаны
- Точные поля в каждом варианте
- Легко добавлять новые
Итоги
- Дискриминатор — общее поле
switchпо дискриминатору сужает типneverдля exhaustive check- Идеально для состояний запроса
- Стандарт для редьюсеров