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-типы».

Sources

Related topics