TypeScriptJuniorTechnical
Объясните union-типы и intersection-типы. Когда использовать каждый?
Union (A | B) — значение одного из типов, используется для опциональных значений и состояний. Intersection (A & B) — значение всех типов сразу, используется для слияния объектных типов и миксинов.
Union-типы и Intersection-типы в TypeScript
Union (A | B) означает «одно из», Intersection (A & B) означает «всё сразу». Это фундаментальные инструменты для моделирования данных в TypeScript.
Union-типы (|)
Значение может быть одним из перечисленных типов:
type StringOrNumber = string | number;
function printId(id: string | number) {
// TypeScript требует narrowing перед использованием специфических методов
if (typeof id === 'string') {
console.log(id.toUpperCase()); // ok: string
} else {
console.log(id.toFixed(2)); // ok: number
}
}
// Дискриминированный union (discriminated union)
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'rect': return shape.width * shape.height;
}
}
Intersection-типы (&)
Значение должно удовлетворять всем типам одновременно:
type Timestamped = { createdAt: Date; updatedAt: Date };
type WithId = { id: string };
type Entity = WithId & Timestamped;
// Entity = { id: string; createdAt: Date; updatedAt: Date }
// Практичный пример: миксины
type AdminUser = User & AdminPermissions;
type ReadonlyRecord<T> = Readonly<T> & { readonly _brand: 'ReadonlyRecord' };
Когда использовать Union
- Значение может быть разных типов:
string | null,number | undefined. - Моделирование состояний:
Loading | Success | Error. - API-ответы с разными формами в зависимости от
status. - Параметры функций, принимающих несколько типов.
Когда использовать Intersection
- Объединение нескольких интерфейсов в один тип.
- Миксины и trait-like паттерны.
- Добавление дополнительных полей к существующему типу без наследования.
Тонкости: примитивы в Intersection
type Strange = string & number; // never — невозможное значение
type Branded = string & { _brand: 'UserId' }; // branded types — полезный паттерн
type UserId = string & { readonly _brand: unique symbol };
function createUserId(raw: string): UserId {
return raw as UserId;
}
Подводные камни
- При intersection объектов с одинаковыми полями разных типов получается
never:{ a: string } & { a: number }→{ a: never }. - Union требует exhaustive check в switch — без
default: assertNever(x)новые кейсы будут молча игнорироваться. - Широкий union
string | number | boolean | objectделает код непрактичным — сужайте через discriminant поля. - Intersection с
anyдаётany, union сanyтоже даётany— это инфекционный тип. - Intersection интерфейсов vs extends:
interface A extends B, Cпредпочтительнее для наследования — даёт лучшие сообщения об ошибках. - Дискриминированный union не работает, если discriminant не является литеральным типом —
type: stringне сужает, нужноtype: 'circle' | 'rect'.
Common mistakes
- Смешивать «union-типы и intersection-типы» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «union-типы и intersection-типы» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет выбор между альтернативой значений и объединением требований.
- Показывает на примере, как работает: union означает
A или Bи требует narrowing, а intersection означает значение, которое удовлетворяет сразу нескольким типам; путаница ломает модели API. - Называет production-нюанс и граничный случай для темы «union-типы и intersection-типы».