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 не Emailconst e = parseEmail(raw);if (e) send(e);Итог: Branded types дают nominal-семантику без runtime-затрат: пара intersection + smart constructor.