Skip to main content

Типи перетину в TypeScript

Тип перетину (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 & BA | 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 окремо означає, що кожен з них можна повторно використовувати в інших компонентах.

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

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

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

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