Discriminated union - об'єднання об'єктних типів зі спільним полем-літералом («дискримінантом»). Перевірка цього поля звужує тип до конкретного варіанта.
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
function render(state: RequestState) {
switch (state.status) {
case 'idle':
return 'Натисніть «Завантажити»';
case 'loading':
return 'Завантаження...';
case 'success':
return `${state.data.length} користувачів`; // data доступне лише тут
case 'error':
return `Помилка: ${state.error}`;
}
}
Чому це краще за набір необов'язкових полів ({ loading?: boolean; data?: User[]; error?: string }): неможливі стани (одночасно data і error, loading і data) стають непредставлюваними на рівні типів.
Перевірка вичерпності: якщо додати новий варіант, компілятор має вказати всі місця, де його не обробили:
function assertNever(value: never): never {
throw new Error(`Необроблений стан: ${JSON.stringify(value)}`);
}
switch (state.status) {
// ...усі case
default:
return assertNever(state);
}
У default після всіх case тип state звузився до never. Додали { status: 'cancelled' } - тепер у default потрапляє цей варіант, і передати його в параметр типу never не можна: помилка компіляції саме там, де треба доробити.
Де застосовують: стани запитів і форм, події та повідомлення (Redux actions, WebSocket-повідомлення), результати операцій ({ ok: true; value } | { ok: false; error }).