Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Що таке структурна типізація і як зробити «номінальні» типи через branded types?

TypeScript порівнює типи за структурою, а не за назвою. Два типи з однаковою формою - взаємозамінні:

type UserId = string;
type OrderId = string;

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

const orderId: OrderId = 'ord_123';
loadUser(orderId);   // жодної помилки - обидва просто string

Аліаси типів - лише інші назви того самого типу. Переплутати ідентифікатори, гроші в копійках і гривнях, сирий і екранований HTML компілятор не завадить.

Branded types додають до типу «мітку», якої не існує під час виконання, але яка робить типи несумісними:

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

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

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

const orderId = 'ord_123' as OrderId;
loadUser(orderId);   // помилка: OrderId несумісний з UserId

Значення з міткою створюють лише в одному місці - функції-конструкторі, що перевіряє дані:

type Email = Brand<string, 'Email'>;

function toEmail(value: string): Email {
  if (!/^[^@\s]+@[^@\s]+$/.test(value)) {
    throw new Error(`Некоректна адреса: ${value}`);
  }
  return value as Email;   // єдине місце з приведенням
}

function sendWelcome(to: Email) { /* тут адреса гарантовано перевірена */ }

Тип Email тепер означає «перевірений рядок» - функції, що його приймають, не мусять перевіряти повторно.

Застосування:

  • ідентифікатори різних сутностей - не передати id замовлення туди, де потрібен id користувача;
  • одиниці виміру: Cents, Uah, Milliseconds, Seconds;
  • перевірені дані: Email, SafeHtml, NonEmptyString, PositiveInt;
  • токени й секрети, які не можна випадково записати в лог як звичайний рядок.

Що варто знати:

  • мітка існує лише в типах - під час виконання це звичайний рядок чи число, без витрат;
  • операції над значенням (id + '_x') повертають звичайний string - мітка губиться, і це правильно;
  • бібліотеки валідації підтримують мітки: z.string().email().brand<'Email'>() у Zod;
  • класи з приватними полями - номінальні за природою: два класи з однаковою формою, але різними #private полями, несумісні.

Докладніше в документації: Сумісність типів

Схожі питання