Типи перетину в 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 конфіг.
Швидкий приклад
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); // 25Person - не набір варіантів. Це один суворий тип, який вимагає одночасно всі властивості з 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. Це не так.
Типові помилки
Очікування, що вкладені властивості перезапишуться:
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'>.
Перетин несумісних примітивів:
type ID = string & number; // never
const id: ID = 'abc'; // ❌ тип 'never' не можна присвоїтиДля брендованих рядків використовуй string & { readonly brand: unique symbol }.
Конфлікт необов'язкового і обов'язкового:
type OptA = { prop?: string };
type ReqB = { prop: number };
type Both = OptA & ReqB; // prop стає обов'язковим numberIntersection завжди бере суворіший варіант. Необов'язкове поступається обов'язковому.
Де зустрічається
- 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.
Приклади
Базовий: об'єднання двох типів об'єкта
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-компонент із об'єднаними пропсами
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 окремо означає, що кожен з них можна повторно використовувати в інших компонентах.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.