Чем Union-типы отличаются от Intersection-типов?

MiddleTypeScript #typescript #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-типом объектов с точки зрения набора допустимых значений?