Discriminated Unions
Discriminated Unions (размеченные объединения) — паттерн, при котором каждый вариант union-типа содержит общее литеральное поле-дискриминатор (обычно
kind,type,status), позволяющее TypeScript точно сужать тип в switch/if без дополнительных type guard.
Зачем нужно
При работе с union-типами TypeScript должен знать, какой именно вариант перед ним. Discriminated union решает это элегантно: одно общее поле с литеральным типом однозначно идентифицирует вариант, и компилятор автоматически сужает тип в ветках switch. Это основной паттерн моделирования состояний и результатов в TS.
Где используется
- Моделирование состояний:
loading | success | error - Паттерн Result/Either:
{ ok: true; data: T } | { ok: false; error: Error } - Redux-подобные action-типы
- AST-узлы, парсеры, компиляторы
- Event-типы в event-driven архитектуре
Основной контент
Базовый пример
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// shape: { kind: "circle"; radius: number }
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "rectangle":
return shape.width * shape.height;
// TypeScript предупредит если пропустить вариант (с exhaustive check)
}
}
Exhaustive check через never
function assertNever(value: never): never {
throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
}
function getArea(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:
return assertNever(shape); // Error если добавили вариант и забыли case
}
}
Паттерн Result
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };
async function fetchUser(id: string): Promise<Result<User>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return { ok: false, error: new Error("Not found") };
return { ok: true, data: await res.json() };
} catch (e) {
return { ok: false, error: e as Error };
}
}
const result = await fetchUser("u-1");
if (result.ok) {
console.log(result.data.name); // TypeScript знает: data есть
} else {
console.error(result.error.message); // TypeScript знает: error есть
}
Состояния загрузки
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; message: string };
function render<T>(state: AsyncState<T>): string {
switch (state.status) {
case "idle": return "Ожидание...";
case "loading": return "Загрузка...";
case "success": return `Данные: ${JSON.stringify(state.data)}`;
case "error": return `Ошибка: ${state.message}`;
}
}
Частые ошибки
- Дискриминатор не литеральный —
kind: stringне даст narrowing; поле должно быть"circle" | "square", а не простоstring. - Разные имена дискриминатора — все варианты должны использовать одно поле (
kind, неtypeв одном иkindв другом). - Забыть exhaustive check — без
assertNeverвdefaultдобавление нового варианта не вызовет ошибку компиляции. - Мутировать дискриминатор — изменение поля
kindв рантайме ломает логику TypeScript.