Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке Discriminated Unions?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Discriminated Union** - це об'єднання (`union`) кількох об'єктних типів, у яких є спільна властивість-мітка (дискримінатор), за якою TypeScript може однозначно визначити, який саме тип перед нами. **Ключове:** завдяки дискримінатору TypeScript автоматично звужує тип у `switch`/`if`, а перевірка `never` у `default` гарантує, що всі варіанти оброблено.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення## Що таке 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 | Звичайний 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 використовує цей паспорт, щоб точно знати, хто перед ним.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.