Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке union в TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Union тип** у TypeScript дозволяє змінній або параметру приймати одне з кількох вказаних значень через оператор `|`. TypeScript вимагає звуження типу перед доступом до специфічних методів. ```typescript type Id = string | number; let userId: Id = "user-123"; // OK userId = 456; // OK ``` **Головне:** спочатку звуж тип через `typeof id === "string"` або перевірку літерала, і тільки потім звертайся до членів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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` підходить ще краще.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.