Що таке union в TypeScript
Union тип у TypeScript дозволяє змінній або параметру приймати одне з кількох вказаних значень, оголошених через оператор |. TypeScript відстежує, який тип активний у кожній точці коду, і вимагає звуження перед зверненням до специфічних членів.
Теорія
TL;DR
- Уявіть мультитул: значення це або ніж, або викрутка, або плоскогубці, одне за раз, але вибір є.
- Union дає вибір (
A | B), а intersection дає комбінацію (A & B). - TypeScript звужує тип автоматично всередині
typeof,instanceofабо перевірок літералів. - Для 2-3 альтернатив підходить union; для складного розгалуження краще discriminated union.
Швидкий приклад
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.
Типові помилки
Доступ до методів без звуження:
let id: string | number = "abc";
id.toFixed(); // Помилка: toFixed не існує для stringВиправлення: if (typeof id === "number") id.toFixed();
Надто широкий union із примітивів:
function process(value: string | number | boolean) {
value.length; // Помилка: length не існує для number | boolean
}Спільних методів для всіх трьох типів немає. Або звужуй явно, або переструктуруй через discriminated union.
Очікування поведінки intersection від union:
type A = { a: string };
type B = { b: number };
let x: A | B = { a: "hi" };
x.b; // Помилка: може бути тип A, у якого немає bA | B означає одне з, а не обидва. Для перетину використовуй A & B.
Пропущений null із fetch-відповідей:
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 як заглушку.
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 достатньо інформації для автоматичного звуження.
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 підходить ще краще.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.