Перейти к содержимому

Branded / Opaque types (advanced pattern)

Способ запретить смешивать «строки разных смыслов» (UserId vs OrderId, Email vs RawString). На интервью просят написать UserId-бренд и валидатор. Красный флаг — использовать обычный type UserId = string и удивляться, что любая строка туда лезет.

Базовый бренд через intersection с фантомным полем:

declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function asUserId(s: string): UserId {
if (!/^u_[a-z0-9]+$/.test(s)) throw new Error('bad UserId');
return s as UserId;
}
const uid: UserId = asUserId('u_abc');
const oid: OrderId = 'o_1' as OrderId;
// const x: UserId = oid; // ошибка: разные бренды

Шаблон smart constructor + branded result:

type Email = Brand<string, 'Email'>;
function parseEmail(s: string): Email | null {
return /.+@.+\..+/.test(s) ? (s as Email) : null;
}
function send(to: Email) { /* ... */ }
const raw = 'a@b.com';
// send(raw); // ошибка: string не Email
const e = parseEmail(raw);
if (e) send(e);

Итог: Branded types дают nominal-семантику без runtime-затрат: пара intersection + smart constructor.