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

Що таке брендовані (номінальні) типи і як вони захищають від плутанини ідентифікаторів?

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

type UserId = number;
type OrderId = number;

function cancelOrder(orderId: OrderId) { /* ... */ }

const userId: UserId = 42;
cancelOrder(userId);   // компілюється - обидва просто number

Переплутані аргументи (transfer(toId, fromId)), id одного ресурсу замість іншого, ціна в копійках замість гривень - типи цього не бачать.

Брендований тип додає до примітиву «позначку», якої немає під час виконання, але яку перевіряє компілятор:

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

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

declare function cancelOrder(id: OrderId): void;

const userId = 42 as UserId;
cancelOrder(userId);   // помилка: тип 'UserId' не сумісний з 'OrderId'

Під час виконання це звичайне число - жодних накладних витрат.

Звідки беруться брендовані значення. as UserId скрізь у коді знищив би користь. Значення має створюватися в одному місці - на межі, після перевірки:

import * as z from 'zod';

const UserIdSchema = z.number().int().positive().brand('UserId');
type UserId = z.infer<typeof UserIdSchema>;

const UserSchema = z.object({ id: UserIdSchema, name: z.string() });
const user = UserSchema.parse(json);   // user.id має тип UserId

Або функція-конструктор з перевіркою: function toCents(uah: number): Cents.

Де брендування найкорисніше:

  • ідентифікатори різних сутностей, які легко переплутати;
  • одиниці виміру: копійки й гривні, мілісекунди й секунди, пікселі й rem;
  • перевірені значення: Email, NonEmptyString, SanitizedHtml - функція, що приймає SanitizedHtml, гарантовано не отримає неочищений рядок;
  • ключі й токени, які не можна передавати в журнали.

Обмеження:

  • арифметика губить бренд: cents + cents - звичайний number, результат треба знову «позначити»;
  • серіалізація: у JSON брендів немає - після JSON.parse значення знову треба перевірити схемою;
  • перебір з брендами ускладнює код без користі - застосовувати там, де помилка дорога (гроші, доступ, ідентифікатори в API).

Докладніше в документації: Zod: брендовані типи

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