Реализуйте UserId, которому не удовлетворяет обычная string
Функция loadUser принимает идентификатор пользователя, но во всех местах вызова ей передают обычную string, а на прошлой неделе передали идентификатор заказа. Дайте UserId собственную идентичность, чтобы компилятор отвергал любую непроверенную строку.
Ограничения: без обёртки времени выполнения и без затрат в порождённом JavaScript; получить UserId можно ровно одним способом, и этот способ обязан проверять сырую строку.
type UserId = string; // замените это
function toUserId(raw: string): UserId {
// ваш код здесь
}
declare function loadUser(id: UserId): void;
Допишите реализацию.
Пересеките базовый тип с фантомным полем по ключу unique symbol — тогда ни одна обычная string в него не присваивается. Откройте единственную фабрику, которая проверяет сырое значение и приводит его к бренду. Бренд живёт только в типах, порождённый JavaScript не меняется.
- ✗Считать, что голый псевдоним
typeилиstring & {}уже исключает обычную строку - ✗Экспортировать бренд, позволяя вызывающим слепить
UserIdвручную черезas - ✗Тянуться к обёртке-class и платить выделением памяти за каждый идентификатор
- →Зачем оставлять символ бренда неэкспортированным из модуля?
- →Как забрендировать идентификатор-
numberиOrderId, чтобы они не совпали?
Решение
Бренд — это фантомное поле, которого нет ни у одной обычной строки. Ключ unique symbol делает его неподделываемым: назвать этот ключ снаружи модуля нельзя.
declare const brand: unique symbol; // не экспортируется
export type UserId = string & { readonly [brand]: 'UserId' };
export function toUserId(raw: string): UserId {
if (!/^u_[0-9a-f]{8}$/.test(raw)) throw new Error(`Bad user id: ${raw}`);
return raw as UserId; // единственный as во всём модуле
}
declare function loadUser(id: UserId): void;
function caller(raw: string) {
loadUser(raw); // ❌ error TS2345: string не присваивается в UserId
loadUser(toUserId(raw)); // ✅ прошло через проверку
}
UserId остаётся строкой во время выполнения: toUserId('u_0000abcd') возвращает ту же самую строку, конкатенация и .length работают как обычно, а as стирается. Затрат — ноль.
⚠️ Отдельный бренд нужен каждому идентификатору (OrderId со своим литералом), иначе два брендированных типа окажутся одинаковыми по структуре и снова станут взаимозаменяемыми.