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

Брендированные типы (Branded Types) в TypeScript

Что такое брендированные типы в 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-интеграциях и при валидации пользовательского ввода.

Поделиться

Поддержать автора

Комментарии:

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

How to: CLI App using Typescript & Webpack

How to: CLI App using Typescript & Webpack

1 мин.

Снял короткую видео инструкцию о том как за 10 шагов создать CLI-приложение используя Typescript и Webpack...

Добавление имени git-ветки в  webpack сборку

Добавление имени git-ветки в webpack сборку

2 мин.

Недавно я столкнулся с тем, что в тестовую сборку нужно было добавить имя текущей git-ветки. Есть 2 способа...

Борьба с грамматическими ошибками в markdown файлах

Борьба с грамматическими ошибками в markdown файлах

3 мин.

Заметил большое количество опечаток в текстах статей. Понял что с этим надо бороться. Вот что из этого вышло...

Подключение счётчика Яндекс Метрики к Next.js приложению

Подключение счётчика Яндекс Метрики к Next.js приложению

13 мин.

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