Union Types

Union Types (A | B) — тип, значение которого может быть одним из нескольких указанных типов; TypeScript требует обрабатывать все варианты или сужать тип перед использованием конкретных методов.

Зачем нужно

Union types точно описывают реальное разнообразие данных: функция может возвращать User | null, параметр принимает string | number, API присылает Success | Error. Без union пришлось бы использовать any или писать несколько перегрузок.

Где используется

  • Функции, принимающие несколько типов аргументов
  • Nullable значения: string | null, User | undefined
  • Результаты операций: Result | Error
  • Discriminated unions для state machine
  • Библиотечные типы: string | number для ID

Основной контент

Базовый синтаксис

type StringOrNumber = string | number;

function format(value: string | number): string {
  // Нельзя сразу вызвать .toFixed() — нужно сужение
  if (typeof value === "string") {
    return value.toUpperCase(); // value: string
  }
  return value.toFixed(2);     // value: number
}

format("hello"); // "HELLO"
format(3.14159); // "3.14"

Nullable типы

// С strict: true, null и undefined нужно явно указывать
function getUser(id: string): User | null {
  return users.find(u => u.id === id) ?? null;
}

const user = getUser("u-1");
// user.name; // Error: Object is possibly 'null'
if (user !== null) {
  console.log(user.name); // OK
}

// Optional chaining + nullish coalescing
const name = getUser("u-1")?.name ?? "Guest";

Union с несколькими типами

type Id = string | number;
type Status = "pending" | "active" | "inactive";
type Value = string | number | boolean | null | undefined;

function processId(id: Id): string {
  return String(id);
}

Сужение union типов

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

// typeof narrowing
function describe(val: string | number): string {
  if (typeof val === "string") return `String: ${val}`;
  return `Number: ${val}`;
}

// Discriminated union narrowing
function area(shape: Shape): number {
  if (shape.kind === "circle") return Math.PI * shape.radius ** 2;
  return shape.side ** 2;
}

// instanceof narrowing
function handleError(err: Error | string): string {
  if (err instanceof Error) return err.message;
  return err;
}

Union в generic context

// Из двух массивов — union элементов
function merge<A, B>(a: A, b: B): (A | B) {
  return [...a, ...b];
}

const mixed = merge([1, 2], ["a", "b"]); // (number | string)

keyof и union

interface User { name: string; email: string; age: number }

type UserKey = keyof User; // "name" | "email" | "age"

function getField(user: User, key: keyof User): User[keyof User] {
  return user[key];
}

Частые ошибки

  • Использовать метод без сужения(val: string | number).toFixed() — ошибка; toFixed есть только у number.
  • string | number | string не упрощается автоматически — нужно самому убрать дубли.
  • Путать | и &| означает «одно из», & означает «оба сразу».
  • Не использовать exhaustive check — если добавить вариант в union и забыть обновить switch — ошибки не будет без assertNever.

Альтернативная позиция: запрет UNION types

"Я запрещаю UNION types в TypeScript".

Это не консенсус, но обоснованная позиция. Краткий список аргументов:

  1. Union → if-ы в коде. Каждый union ведёт к рантайм-проверкам typeof / instanceof. Это слипшиеся абстракции, которые надо было разделить.

    // Плохо — Запрещается
    type Body = ArrayBuffer | Buffer | string;
    function send(body: Body) {
      if (body instanceof ArrayBuffer) { /* ... */ }
      else if (body instanceof Buffer) { /* ... */ }
      else if (typeof body === 'string') { /* ... */ }
    }
    
  2. Union прячет смешение абстракций. function pay(money: number | string | Money) — это три разных контракта, склеенных в один параметр.

  3. Плохой union ломает мономорфность V8 — оптимизатор не может зафиксировать форму, hidden classes расходятся, deopt.

  4. Когнитивная нагрузка. Читатель должен в уме перебрать все варианты ветвления.

Допустимые union (по автору)

  • ✅ Union строковых литералов: 'pending' | 'active' | 'done' — V8 видит просто string.
  • ✅ Union чисел/состояний: коды ошибок, HTTP-методы.
  • ✅ Union типов с общим интерфейсом: Buffer | null (оба object), MailTarget = User | Company (оба имеют email).
  • ✅ Overload-функции (формально union, но через сигнатуры).
  • ✅ Thenable: Promise<T> | T.

Запрещённый union (по автору)

  • ❌ Несвязанные типы: User | Socket | string.
  • name: string | string — лучше всегда names: string.
  • money: number | string | Money — выбери одну форму.

"Если union-type, значит где-то логика уехала в if-ы."

Связанные темы

Источники

  • 🎓 Я запрещаю UNION types в TypeScript и ORM · 2025-12-17 · YouTube
    • Тезисы: union → if-ы; union строк ок для V8; union несвязанных типов — антипаттерн.
    • Позиция автора (provocative): "Я запрещаю UNION types". Допустимы только union по общему контракту/интерфейсу или union литералов.

Ресурсы