Что такое branded types в TypeScript?

SeniorTypeScript · Frontend·Обновлено 27 августа 2026
Коротко
Branded types (номинальные типы) — приём, при котором к обычному типу (например, string или number) добавляется уникальная «метка», делающая его несовместимым с другими типами той же структуры. Это позволяет эмулировать номинативную типизацию поверх структурной системы типов TypeScript.

Проблема структурной типизации

TypeScript использует структурную (утиную) типизацию: два типа считаются совместимыми, если у них одинаковая форма. Это удобно, но создаёт проблему для доменных примитивов:

type UserId = string;
type OrderId = string;

function getUser(id: UserId) { /* ... */ }

const orderId: OrderId = "order-1";
getUser(orderId); // компилируется, хотя логически это ошибка

Оба типа — просто string, поэтому компилятор не видит разницы между UserId и OrderId.

Идея branded types

К типу добавляется уникальное фантомное поле («бренд»), которого не существует в рантайме, но которое различает типы на уровне компилятора:

type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

Теперь UserId и OrderId структурно различны, и присвоить одно вместо другого напрямую нельзя — нужно явное приведение.

Создание значений безопасно

Поскольку бренд — фиктивное поле, значения создают через функции-конструкторы с валидацией:

function createUserId(raw: string): UserId {
  if (!raw.startsWith("user_")) {
    throw new Error("Некорректный формат UserId");
  }
  return raw as UserId;
}

Единственное место, где используется as — сам конструктор. Дальше по коду тип защищён компилятором.

unique symbol вместо строкового литерала

Строковый бренд теоретически можно подделать вручную ({ ... } as UserId), поэтому для более строгой изоляции используют unique symbol:

declare const userIdBrand: unique symbol;
type UserId = string & { readonly [userIdBrand]: true };

unique symbol не экспортируется наружу модуля, что затрудняет создание значения обходным путём вне контролируемого конструктора.

Где применяется

  • Идентификаторы сущностей: UserId, OrderId, ProductId, чтобы не перепутать их местами в сигнатурах функций.
  • Единицы измерения: Meters, Seconds, Cents — предотвращает сложение метров с секундами.
  • Провалидированные строки: Email, NonEmptyString, PositiveInt — тип служит доказательством того, что валидация уже прошла (parse, don't validate).

Ограничения

  • Бренд существует только на уровне типов — в рантайме это обычная строка или число, никакой реальной защиты от неправильных данных из JSON или API нет.
  • Требует дисциплины: разработчики должны создавать значения только через конструкторы, а не через as в произвольном месте кода.
  • Добавляет шаблонный код и когнитивную нагрузку — оправдано в доменных ядрах с большим количеством похожих примитивов, но избыточно для мелких скриптов.

Итог

Branded types — способ получить номинативную типизацию без изменения рантайм-поведения, распространённый паттерн в TypeScript-кодовых базах с DDD-подходом, где важно различать примитивы одинаковой формы, но разного смысла.

Что хочет услышать интервьюер

Объяснение, что TypeScript использует структурную типизацию и в чём её ограничение для доменных примитивов

Понимание механизма: пересечение типа с фантомным полем (`& { __brand: ... }` или `unique symbol`)

Знание, что бренд существует только в типах, а не в рантайме, и не заменяет реальную валидацию

Пример практического применения: ID сущностей, единицы измерения, провалидированные строки

Упоминание безопасного создания значений через конструкторы, а не через произвольный `as`

Пример: Базовая реализация branded type

type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function getUser(id: UserId) {
  // ...
}

function createUserId(raw: string): UserId {
  if (!raw.startsWith("user_")) {
    throw new Error("Некорректный формат UserId");
  }
  return raw as UserId;
}

const uid = createUserId("user_42");
getUser(uid); // ок

const oid = "order_1" as OrderId;
getUser(oid); // ошибка компиляции: OrderId не совместим с UserId

Пример: Более строгий бренд через unique symbol

declare const emailBrand: unique symbol;
type Email = string & { readonly [emailBrand]: true };

function parseEmail(raw: string): Email {
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw);
  if (!isValid) {
    throw new Error("Невалидный email");
  }
  return raw as Email;
}

function sendWelcome(email: Email) {
  // сюда попадает только уже провалидированная строка
}

sendWelcome(parseEmail("user@example.com")); // ок
// sendWelcome("user@example.com"); // ошибка компиляции: string не Email

Типичные ошибки

Утверждение, что branded types — встроенная фича TypeScript, а не паттерн поверх пересечения типов

Путаница с nominal typing в других языках без объяснения, что в TS это эмуляция, а не встроенный механизм

Забывают сказать, что бренд не проверяется в рантайме и не защищает от некорректных данных из внешних источников

Использование `as Brand` в произвольных местах кода вместо единой контролируемой функции-конструктора

Не могут привести конкретный пример из практики (ID, единицы измерения, email/непустая строка)

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Feature-Sliced Design

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.5
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее