Брендированные типы (branded types) в TypeScript

Что такое branded types в Typescript и как с их помощью улучшить type safety в рантайме.

Брендированные типы (branded types) в TypeScript
Дата публикации
08.07.25
Дата обновления
16.05.26
Время чтения
4 мин.

Что такое брендированные типы в TypeScript?

Брендированные типы — это паттерн в TypeScript, позволяющий повысить безопасность типов, добавляя уникальные метки к существующим типам (например, string или number).

Это особенно полезно, когда несколько значений имеют одинаковый базовый тип, но несут разную семантическую нагрузку — например, UserId, PostId, OrderId.

Несмотря на то что на уровне выполнения это всё те же строки или числа, компилятор TypeScript воспринимает их как разные типы. Это предотвращает случайную подстановку одного значения вместо другого.

Зачем нужны брендированные типы?

TypeScript обеспечивает структурную типизацию — тип считается корректным, если у него есть нужные поля, независимо от его имени. Это может привести к ошибкам, когда два значения совместимы по структуре, но не по смыслу. Например:

type User = { id: string, name: string }
type Post = { id: string, ownerId: string }

function getPostByUser(userId: string, postId: string) { ... }

// вызов
getPostByUser(post.id, user.id) // ❌ ошибка смысла, но TypeScript не ругается

С брендированными типами TypeScript «поймёт» ошибку на этапе компиляции:

type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }

function getPostByUser(userId: UserId, postId: PostId) { ... }

getPostByUser(post.id, user.id) // ❌ ошибка типов: PostId ≠ UserId

Как создать брендированный тип?

Существует несколько подходов. Простейший — через пересечение типов с объектом-брендом:

type Brand<K, T> = K & { __brand: T };

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

Чтобы избежать дублирования имён и скрыть бренд от автодополнения в редакторе, можно использовать unique symbol:

declare const __brand: unique symbol;

type Brand<T, B> = T & { [__brand]: B };

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

Преимущества брендированных типов

  • Повышенная безопасность типов (type safety): предотвращают подстановку несовместимых значений.
  • 📖 Ясность кода: код становится самодокументируемым — UserId, OrderId говорят сами за себя, упрощают понимание, поддержку и рефакторинг кода
  • 🧪 Простота тестирования и валидации: можно создавать отдельные функции-валидаторы, возвращающие брендированный тип только при успешной проверке.

Ограничения

  • 🧱 Бренды — это compile-time метки: во время выполнения они не существуют.
  • 💡 Можно случайно обойти типизацию, используя as без валидации — будьте осторожны.
  • 🧠 Требуется внимание разработчиков: особенно в больших командах важно соблюдать договорённости при создании брендированных типов.

Применение на практике

1. Разделение ID-шников

type UserId = Brand<string, 'UserId'>
type PostId = Brand<string, 'PostId'>

function getComments(postId: PostId, authorId: UserId) { ... }

getComments(user.id, post.id) // ❌ TypeScript выдаст ошибку

2. Валидация пользовательского ввода

type EmailAddress = Brand<string, "EmailAddress">;

function validateEmail(input: string): EmailAddress {
  // Не валидируйте так email! Этот код написан для примера.
  if (!input.includes("@")) throw new Error("Invalid email");
  return input as EmailAddress;
}

3. Работа с числовыми значениями, например, возрастом

type Age = Brand<number, "Age">;

function createAge(age: number): Age {
  if (age < 0 || age > 125)
    throw new Error("Возраст вне допустимого диапазона");
  return age as Age;
}

function getBirthYear(age: Age): number {
  return new Date().getFullYear() - age;
}

const myAge = createAge(30);
const birthYear = getBirthYear(myAge);

Заключение

Branded Types — это мощный приём TypeScript, позволяющий повысить читаемость, надёжность и безопасность кода. Несмотря на то, что они не влияют на выполнение, их сила в compile-time проверках, помогающих предотвратить трудноуловимые ошибки.

Используйте брендированные типы там, где одинаковые по структуре значения имеют разное семантическое значение. Это особенно важно в больших приложениях, API-интеграциях и при валидации пользовательского ввода.

На этом всё. Спасибо за внимание!

Расскажите о вашем проекте

Связаться иначе

Часто задаваемые вопросы

Это паттерн: к базовому типу (string, number) добавляют уникальную «метку» (brand), чтобы UserId и PostId для компилятора были разными типами, хотя в рантайме оба остаются строками.

TypeScript сравнивает типы по структуре, а не по имени. Два поля id: string взаимозаменяемы, и можно перепутать аргументы в вызове. Бренд заставляет компилятор отловить post.id там, где ждут UserId.

Нет. Метка живёт только на этапе компиляции и исчезает после transpile в JavaScript. Надёжность в рантайме даёт не «магия» бренда, а валидация в функциях, которые возвращают брендированный тип.

Через пересечение: type UserId = string & { __brand: 'UserId' } или обобщённый алиас Brand с двумя параметрами типа. Удобный вариант — unique symbol вместо строкового __brand, чтобы не светить бренд в автодополнении (примеры в статье).

Для ID и других «голых» примитивов с разным смыслом, для Email, URL, возраста после проверки, в API-слоях и больших кодовых базах — везде, где путаница двух string обходится дорого.

Да, если нет реальной проверки: as EmailAddress обходит защиту. Правильный путь — функция validateEmail, которая бросает ошибку или возвращает бренд только после валидации (как в примере статьи).

Бренд не добавляет полей и методов — это всё тот же примитив. Для сложных сущностей с поведением по-прежнему нужны интерфейсы, type с полями или классы; бренды дополняют, а не заменяют модель данных.

Вам может быть интересно

Автоматизируем публикацию npm-пакета с помощью Github Actions
Автоматизируем публикацию npm-пакета с помощью Github Actions

Автоматизируем публикацию npm-пакета с помощью Github Actions

7 мин.

Инструкция по подготовке npm-пакета к автоматизированному релизу с помощью Github Actions...

Пост#DevOps#github
Добавляем Telegram Widgets в React-приложение
Добавляем Telegram Widgets в React-приложение

Добавляем Telegram Widgets в React-приложение

3 мин.

Инструкция по добавлению Telegram Widgets в React-приложение с помощью написанного мной npm-пакета react-telegram-widgets...

Пост#react#инструменты
Настройка MAX-бота для работы в Yandex Cloud Functions
Настройка MAX-бота для работы в Yandex Cloud Functions

Настройка MAX-бота для работы в Yandex Cloud Functions

4 мин.

Инструкция по настройке и хостингу простейшего MAX-бота в Yandex Cloud Functions.

Пост#yandex#max
Добавляем поддержку MDX в Next.js приложение
Добавляем поддержку MDX в Next.js приложение

Добавляем поддержку MDX в Next.js приложение

3 мин.

Инструкция по добавлению поддержки MDX в Next.js приложение...

Пост#nextjs
Добавляем подсветку кода (синтаксиса) в статический блог на Next.js
Добавляем подсветку кода (синтаксиса) в статический блог на Next.js

Добавляем подсветку кода (синтаксиса) в статический блог на Next.js

4 мин.

Инструкция по добавлению подсветки кода (синтаксиса) в статическом блоге на Next.js...

Пост#туториалы#nextjs
Добавляем Not Found (404) страницу в Next.js приложение
Добавляем Not Found (404) страницу в Next.js приложение

Добавляем Not Found (404) страницу в Next.js приложение

3 мин.

Инструкция по добавлению Not Found (404) страницы в Next.js приложение...

Пост#nextjs#seo
NPM пакет @baranov-guru/react-telegram-widgets
NPM пакет @baranov-guru/react-telegram-widgets

NPM пакет @baranov-guru/react-telegram-widgets

Бесплатный NPM-пакет для интеграции Telegram Widgets в React-приложения.

Проект#react#telegram
Frontend разработка
Frontend разработка

Frontend разработка

Разрабатываем интерфейсы, которые быстро работают, удобно используются и масштабируются вместе с продуктом.

Услуга#frontend#веб-разработка
Backend разработка
Backend разработка

Backend разработка

Разрабатываем серверную часть, которая стабильно работает под нагрузкой и масштабируется вместе с продуктом.

Услуга#backend#веб-разработка