Чем Union-типы отличаются от Intersection-типов?
Вопрос
В чём разница между union-типами (A | B) и intersection-типами (A & B)?
Короткий ответ
Union (|) означает "значение одного из перечисленных типов" — доступны только общие для всех вариантов свойства, пока тип не сужен. Intersection (&) означает "значение, удовлетворяющее всем перечисленным типам одновременно" — объединяет их свойства в одном объекте.
Подробный ответ
Union-тип описывает значение, которое может быть одним из нескольких типов. Работать с union напрямую можно только с тем, что есть у всех вариантов сразу (пересечение возможностей), — для доступа к специфичным полям нужно сначала сузить тип (type narrowing):
type Id = string | number
function printId(id: Id) {
console.log(id.toString()) // OK — toString есть и у string, и у number
// id.toUpperCase() // Ошибка — есть только у string
}
Intersection-тип объединяет несколько типов в один, у которого есть все поля каждого из них. Если типы объектные, значение должно соответствовать обоим одновременно:
interface Timestamped { createdAt: Date }
interface Named { name: string }
type NamedEntity = Timestamped & Named
// объект обязан иметь И createdAt, И name
const entity: NamedEntity = { createdAt: new Date(), name: 'Item 1' }
Важный нюанс: если пересекаются примитивы с несовместимыми типами, результат — never, потому что не существует значения, одновременно являющегося и string, и number:
type Impossible = string & number // never
А для объектных типов с одинаковым именем свойства, но разными типами значений, свойство в пересечении тоже схлопывается в never:
type A = { value: string }
type B = { value: number }
type C = A & B // { value: never }
Частый практический паттерн — discriminated union (объединение с общим полем-дискриминатором), которое сочетает удобство union (гибкость вариантов) с возможностью безопасного сужения через switch/if по полю-метке.
Пример
// Union: результат запроса — один из двух вариантов
type RequestState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string }
function render(state: RequestState<string[]>) {
switch (state.status) {
case 'loading': return 'Загрузка...'
case 'success': return state.data.join(', ') // data доступен только тут
case 'error': return `Ошибка: ${state.message}`
}
}
// Intersection: объединяем базовые пропсы кнопки с пропсами конкретного варианта
interface BaseButtonProps { onClick: () => void; disabled?: boolean }
interface IconButtonProps { icon: string; 'aria-label': string }
type IconButton = BaseButtonProps & IconButtonProps
const btn: IconButton = {
onClick: () => console.log('clicked'),
icon: 'trash',
'aria-label': 'Удалить',
}
Дополнительные вопросы
- Что такое discriminated union и почему для него важно единое литеральное поле-дискриминатор?
- Как TypeScript обрабатывает пересечение (
&) двух функциональных типов с разными сигнатурами? - В чём разница между union-типом объектов и intersection-типом объектов с точки зрения набора допустимых значений?