Skip to main content

Що таке union в TypeScript

Union тип у TypeScript дозволяє змінній або параметру приймати одне з кількох вказаних значень, оголошених через оператор |. TypeScript відстежує, який тип активний у кожній точці коду, і вимагає звуження перед зверненням до специфічних членів.

Теорія

TL;DR

  • Уявіть мультитул: значення це або ніж, або викрутка, або плоскогубці, одне за раз, але вибір є.
  • Union дає вибір (A | B), а intersection дає комбінацію (A & B).
  • TypeScript звужує тип автоматично всередині typeof, instanceof або перевірок літералів.
  • Для 2-3 альтернатив підходить union; для складного розгалуження краще discriminated union.

Швидкий приклад

typescript
type Id = string | number; let userId: Id = "user-123"; // OK: string userId = 456; // OK: number userId = true; // Помилка: boolean не входить в union // TypeScript звужує тип всередині блоку if (typeof userId === "string") { console.log(userId.toUpperCase()); // методи string доступні тут }

Після перевірки TypeScript знає, що userId це string всередині блоку. Зовні він залишається string | number.

Головна відмінність

Union дає компілятору вибір: значення є рівно одним із перелічених типів, а не всіма одночасно. Щоб звернутися до специфічних методів або властивостей, потрібно спочатку звузити тип через typeof, instanceof або перевірку літерала. Intersection (&) працює навпаки: значення має відповідати всім типам одночасно.

Коли використовувати

  • Поле відповіді API може бути string | null → union.
  • Параметр конфігурації приймає number | boolean → union.
  • Дані події змінюються залежно від типу → discriminated union із полем kind або type.
  • Чотири або більше альтернатив зі складним звуженням → розглянь branded types.

Як TypeScript аналізує union-типи

Під час компіляції TypeScript відстежує, які типи ще можливі в кожній точці коду. Це називається аналізом потоку керування (control flow analysis). Коли ти пишеш if (typeof x === "string"), компілятор звужує x до string всередині блоку і прибирає цю гілку з залишкового типу зовні. В рантаймі нічого цього немає. Union типи повністю стираються після компіляції, залишаючи звичайні значення JavaScript.

Типові помилки

Доступ до методів без звуження:

typescript
let id: string | number = "abc"; id.toFixed(); // Помилка: toFixed не існує для string

Виправлення: if (typeof id === "number") id.toFixed();

Надто широкий union із примітивів:

typescript
function process(value: string | number | boolean) { value.length; // Помилка: length не існує для number | boolean }

Спільних методів для всіх трьох типів немає. Або звужуй явно, або переструктуруй через discriminated union.

Очікування поведінки intersection від union:

typescript
type A = { a: string }; type B = { b: number }; let x: A | B = { a: "hi" }; x.b; // Помилка: може бути тип A, у якого немає b

A | B означає одне з, а не обидва. Для перетину використовуй A & B.

Пропущений null із fetch-відповідей:

typescript
async function getName(): Promise<string> { // Неправильно const res = await fetch("/name"); return res.json(); // Насправді Promise<string | null> }

Виправлення: оголоси як Promise<string | null> і додай перевірку на null перед поверненням.

Де зустрічається в реальному коді

  • React: ReactNode = null | string | number | ReactElement (тип children).
  • Express: параметри маршруту типізуються як string | undefined.
  • Node.js: колбек fs.readFile отримує NodeJS.ErrnoException | null першим аргументом.
  • tRPC: discriminated union-и для типізованих відповідей із помилками.

Питання для співбесіди

Q: Що таке discriminated union?
A: Union, де кожен член має спільне поле з літеральним значенням, наприклад kind: "not-found". TypeScript звужує тип автоматично в if або switch на основі цього поля, без додаткових перевірок.

Q: Union проти any: в чому різниця?
A: any повністю відключає перевірку типів. Union зберігає її: до специфічних членів можна звертатися лише після звуження, тому помилки видно ще на етапі компіляції.

Q: Чи можна вкладати union-и?
A: Так. (string | number[]) | boolean валідний запис. Компілятор розгортає його внутрішньо для перевірки.

Q: Чи є вплив на продуктивність у рантаймі?
A: Жодного. Union типи стираються при компіляції і існують лише під час статичного аналізу.

Q: Як control flow analysis працює з оператором in у TypeScript 4.9+?
A: if ("prop" in obj) звужує obj до типів, які включають цю властивість. Для {a: 1} | {b: 2} після if ("a" in obj) TypeScript знає, що obj має a. Корисно, коли немає спільного поля-дискримінанта.

Приклади

Необов'язкове зображення у React-компоненті

Проп, який приймає URL завантаженого зображення або null як заглушку.

typescript
interface ImageProps { src: string | null; // завантажене зображення або заглушка alt: string; } function Image({ src, alt }: ImageProps) { return ( <img src={src || "placeholder.png"} // null обробляється безпечно alt={alt} /> ); } <Image src="photo.jpg" alt="Кіт" />; // string <Image src={null} alt="Зображення відсутнє" />; // null → заглушка

Union робить необов'язковий стан явним. Без null у типі TypeScript відхилив би другий виклик ще під час компіляції.

Discriminated union для обробки помилок API

Коли API повертає помилки різних форм, поле kind дає TypeScript достатньо інформації для автоматичного звуження.

typescript
type ApiError = | { kind: "validation"; message: string; field: string } | { kind: "not-found"; id: number } | { kind: "server"; status: number }; function handleError(error: ApiError) { if (error.kind === "validation") { console.log(error.field); // OK, тип звужено до validation } else if (error.kind === "not-found") { console.log(error.id); // OK, тип звужено до not-found } // error.message поза гілкою → Помилка: не всі члени мають це поле }

Цей патерн зламується, коли розробник додає новий член union, але забуває обробити його в ланцюжку if/else. Фінальний else із викидом помилки робить такі прогалини помітними в рантаймі. Для вичерпної перевірки на етапі компіляції switch із never-асерцією в default підходить ще краще.

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

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

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

Дочитали статтю?