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).