Що таке Discriminated Unions?
Що таке Discriminated Union
Discriminated Union (дослівно: дискримінантне об'єднання) -
це об'єднання (union) кількох об'єктних типів,
у яких є спільна властивість-мітка (дискримінатор),
за якою TypeScript може однозначно визначити, який саме тип перед нами.
Приклад
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
side: number;
};
type Shape = Circle | Square;Тут:
Shape- це об'єднання двох типів (Circle | Square),- в обох є спільна властивість
kind, - значення
kindунікальне для кожного типу.
Це і є дискримінантне поле (discriminant).
Навіщо потрібен "дискримінатор"
TypeScript вміє звужувати тип (narrowing) за значенням властивості kind.
function area(shape: Shape) {
if (shape.kind === "circle") {
// Тут shape: Circle
return Math.PI * shape.radius ** 2;
} else {
// Тут shape: Square
return shape.side ** 2;
}
}TypeScript бачить shape.kind === "circle"
і автоматично визначає, що shape тепер Circle.
Як це працює під капотом
TypeScript робить так:
- Знаходить властивість, спільну для всіх членів об'єднання (
kind); - Перевіряє, що вона приймає унікальні літеральні значення (
"circle","square"); - Коли ти робиш перевірку
shape.kind === "circle", TS виключає всі інші варіанти (Square).
Це і є discriminated union narrowing - автоматичне звуження типу за дискримінатором.
Приклад з кількома варіантами
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}У блоці switch TypeScript сам звузить тип shape на кожному кроці.
А never у default гарантує, що всі випадки оброблені.
Чому це зручно
Discriminated unions дозволяють:
- створювати безпечні enum-подібні конструкції;
- типізувати стани застосунку, результати API, Redux-actions;
- уникати зайвих
asі ручних перевірок; - гарантувати вичерпну обробку всіх випадків.
Реальні приклади
1. Стани завантаження (state machine pattern)
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string };
type ErrorState = { status: "error"; message: string };
type RequestState = LoadingState | SuccessState | ErrorState;
function render(state: RequestState) {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Data: ${state.data}`;
case "error":
return `Error: ${state.message}`;
}
}2. Дії у стилі Redux
type Action =
| { type: "add"; payload: number }
| { type: "remove"; id: string }
| { type: "reset" };
function reducer(state: number[], action: Action) {
switch (action.type) {
case "add":
return [...state, action.payload];
case "remove":
return state.filter((_, i) => i.toString() !== action.id);
case "reset":
return [];
}
}3. Відповіді API
type ApiResponse<T> =
| { kind: "ok"; data: T }
| { kind: "error"; message: string };
function handleResponse(res: ApiResponse<string>) {
if (res.kind === "ok") {
console.log("OK:", res.data);
} else {
console.error("Error:", res.message);
}
}Перевірка на повноту (never exhaustiveness check)
Корисний прийом: додати перевірку, що всі варіанти оброблено.
function checkExhaustive(value: never): never {
throw new Error("Unhandled case: " + value);
}
function draw(shape: Shape) {
switch (shape.kind) {
case "circle":
return "Drawing circle";
case "square":
return "Drawing square";
default:
return checkExhaustive(shape); // якщо з'явиться новий варіант - TS видасть помилку
}
}Discriminated unions проти звичайних unions
| Звичайний union | Discriminated union |
|---|---|
| `type A = {a: number} | {b: string}` |
| Немає спільної ознаки | Є спільне поле-мітка (kind) |
| TS не знає, який варіант активний | TS може звужувати тип автоматично |
Потребує ручних перевірок ('a' in obj) | Перевірка за kind чи switch |
Підсумок
| Термін | Значення |
|---|---|
| Discriminated union | Об'єднання типів зі спільним дискримінатором |
| Дискримінатор (discriminant) | Спільна властивість з унікальними літеральними значеннями |
| Навіщо потрібно | Дозволяє TypeScript автоматично звужувати тип і перевіряти всі випадки |
| Типове застосування | State-машини, Redux actions, відповіді API, обробка помилок |
| Головна перевага | Безпечне розгалуження без as, з повною типізацією і підказками |
Просто запам'ятай:
Discriminated Union - це коли в різних форм даних є "паспорт" (дискримінатор), і TypeScript використовує цей паспорт, щоб точно знати, хто перед ним.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.