Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Типи перетину в TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Тип перетину (intersection type)** об'єднує кілька типів в один за допомогою `&`. Значення має задовольняти всі типи одночасно. ```typescript type Person = { name: string } & { age: number }; const alice: Person = { name: "Alice", age: 25 }; // обидва поля обов'язкові ``` **Ключове:** конфліктні властивості на кшталт `string & number` стають `never`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Тип перетину (intersection type)** - TypeScript-тип, який створюється за допомогою `&` і вимагає від значення задовольняти всі об'єднані типи одночасно. ## Теорія ### TL;DR - Аналогія: швейцарський армійський ніж. Всі інструменти окремих гаджетів упаковані в один об'єкт. Отримуєш всі можливості, але об'єкт зобов'язаний нести їх усі. - `&` вимагає ВСІХ властивостей з кожного типу. `|` вимагає збігу хоча б з одним. - Вкладені типи перетинаються глибоко, не поверхово. `{data: {x: number}} & {data: {y: string}}` дає `{data: {x: number; y: string}}`, а не заміну. - Конфліктні примітиви (`string & number`) стають `never`. Це помилка компіляції, не runtime. - Використовуй `&`, щоб склеювати форми об'єктів: props + theme, user + admin, базовий конфіг + db конфіг. ### Швидкий приклад ```typescript type Name = { name: string }; type Age = { age: number }; type Person = Name & Age; // потрібно мати І name, І age const alice: Person = { name: "Alice", age: 25 }; // ✅ ок const bob: Person = { name: "Bob" }; // ❌ Помилка: відсутній 'age' console.log(alice.name); // "Alice" console.log(alice.age); // 25 ``` `Person` - не набір варіантів. Це один суворий тип, який вимагає одночасно всі властивості з `Name` і `Age`. ### Головна відмінність від union-типів З `A | B` значення має збігатися лише з одним типом. З `A & B` воно повинно задовольняти обидва одночасно. Тому з union доступ до `propA` часто потребує type guard, а з intersection `propA` і `propB` завжди доступні напряму. ### Коли використовувати - Ролі в системі: `Admin & User`, де адмін має унікальні поля, але й усі поля звичайного юзера. - Розширення Express-запиту: `Request & { user: User }` в auth middleware. - React-пропси: `ButtonProps & ThemeProps`, коли один компонент приймає обидва набори. - Не варто використовувати, якщо типи мають одну й ту саму властивість з несумісними типами значень. Така властивість стане `never`. ### Перетин проти union | Аспект | `A & B` | `A \| B` | |---|---|---| | Вимога | Відповідати всім типам | Відповідати хоча б одному | | Доступні властивості | Всі з A і всі з B | Лише спільні гарантовані | | Безпека доступу | `obj.propA` і `obj.propB` завжди доступні | Потрібен type guard: `if ('propA' in obj)` | | Конфліктна властивість | Стає `never` | Не застосовується | | Коли використовувати | Компонування об'єктів | Опис альтернатив | ### Як компілятор це обробляє TypeScript обчислює `A & B` виключно під час компіляції. Runtime-вартості немає. Компілятор об'єднує обов'язкові властивості з усіх типів у результат. Необов'язкові залишаються необов'язковими. Якщо два типи мають однакову властивість з несумісними типами значень, ця властивість стає `never`. Скомпільований JavaScript бачить звичайні об'єкти, без жодних слідів intersection-логіки. Важливий нюанс, який регулярно плутає людей: TypeScript перетинає вкладені типи глибоко. `{ data: { x: number } } & { data: { y: string } }` дає `{ data: { x: number; y: string } }`, а не заміну поля `data`. Багато розробників вважають, що `B` перезаписує `A`, як у `Object.assign`. Це не так. ### Типові помилки **Очікування, що вкладені властивості перезапишуться:** ```typescript type A = { data: { x: number } }; type B = { data: { y: string } }; type Merged = A & B; // data: { x: number; y: string } -- НЕ просто { y: string } const bad: Merged = { data: { x: 1 } }; // ❌ відсутній 'y' const ok: Merged = { data: { x: 1, y: 'hi' } }; // ✅ ``` Якщо потрібна поведінка перезапису, використовуй `Pick<B, 'data'> & Omit<A, 'data'>`. **Перетин несумісних примітивів:** ```typescript type ID = string & number; // never const id: ID = 'abc'; // ❌ тип 'never' не можна присвоїти ``` Для брендованих рядків використовуй `string & { readonly brand: unique symbol }`. **Конфлікт необов'язкового і обов'язкового:** ```typescript type OptA = { prop?: string }; type ReqB = { prop: number }; type Both = OptA & ReqB; // prop стає обов'язковим number ``` Intersection завжди бере суворіший варіант. Необов'язкове поступається обов'язковому. ### Де зустрічається - React: `ButtonProps & { variant: 'primary' | 'secondary' }` в бібліотеках компонентів типу Material-UI. - Express: `Request & { user: User }` у Passport.js middleware. - Redux Toolkit: `PayloadAction<string> & { meta: { timestamp: number } }` для типізованих екшенів. - Zod: `z.intersection(schemaA, schemaB)` - аналогічна поведінка на рівні схем валідації. ### Питання на співбесіді **Q:** Яка різниця між `type A = B & C` і `interface A extends B, C`? **A:** Обидва варіанти дають еквівалентні типи в більшості випадків. `interface` підтримує declaration merging - його можна доповнити новими властивостями пізніше. `type` фіксується після оголошення. Для об'єктних структур у бібліотеках `interface` зазвичай гнучкіший. **Q:** Що відбувається з `string & number`? **A:** Результат - `never`. Жодне значення не може задовольнити обидва примітивних типи одночасно, тому тип стає неможливим до присвоєння. **Q:** Чи можна перетинати union з іншим типом? **A:** Так. TypeScript дистрибутивно розкладає: `(A | B) & C` стає `(A & C) | (B & C)`. Зручно, коли треба звузити union. **Q:** Чи є runtime-вплив? **A:** Жодного. TypeScript видаляє всі типи під час компіляції. V8 бачить звичайний JavaScript. ## Приклади ### Базовий: об'єднання двох типів об'єкта ```typescript type Address = { street: string; city: string; }; type ContactInfo = { email: string; phone: string; }; type UserProfile = Address & ContactInfo; const user: UserProfile = { street: 'вул. Хрещатик, 1', city: 'Київ', email: 'user@example.com', phone: '+380 67 000 0000', }; // всі чотири поля обов'язкові ``` Пропусти одне поле - компілятор одразу вкаже на помилку. Без жодних runtime-перевірок. ### Середній: React-компонент із об'єднаними пропсами ```typescript interface ButtonProps { onClick: () => void; children: React.ReactNode; } interface ThemeProps { variant: 'primary' | 'secondary'; size: 'small' | 'large'; } type ThemedButtonProps = ButtonProps & ThemeProps; const ThemedButton: React.FC<ThemedButtonProps> = ({ onClick, children, variant, size, }) => ( <button className={`${variant} ${size}`} onClick={onClick}> {children} </button> ); // <ThemedButton variant="primary" size="large" onClick={() => {}}>Submit</ThemedButton> ``` Пропусти `variant` або `onClick` - TypeScript одразу покаже помилку. Автодоповнення показує всі чотири пропси. Зберігати `ButtonProps` і `ThemeProps` окремо означає, що кожен з них можна повторно використовувати в інших компонентах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.