Skip to main content

Що таке Discriminated Unions?

Що таке Discriminated Union

Discriminated Union (дослівно: дискримінантне об'єднання) - це об'єднання (union) кількох об'єктних типів, у яких є спільна властивість-мітка (дискримінатор), за якою TypeScript може однозначно визначити, який саме тип перед нами.


Приклад

javascript
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.

javascript
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 робить так:

  1. Знаходить властивість, спільну для всіх членів об'єднання (kind);
  2. Перевіряє, що вона приймає унікальні літеральні значення ("circle", "square");
  3. Коли ти робиш перевірку shape.kind === "circle", TS виключає всі інші варіанти (Square).

Це і є discriminated union narrowing - автоматичне звуження типу за дискримінатором.


Приклад з кількома варіантами

javascript
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)

javascript
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

javascript
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

javascript
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)

Корисний прийом: додати перевірку, що всі варіанти оброблено.

javascript
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

Звичайний unionDiscriminated 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 використовує цей паспорт, щоб точно знати, хто перед ним.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.