Номинальные типы в структурном TypeScript
Вопрос
TypeScript структурный. Как сделать так, чтобы UserId и OrderId (оба строки) нельзя было перепутать? Какие есть способы и их цена?
Попробуйте сами
Ответьте вслух за 2 минуты, как на собесе. Потом сверьтесь с тезисами: что прозвучало, а что нет.
Показать ответ и разбор
Коротко
Что должно прозвучать в ответе
Проблема: структурная типизация считает одинаковые формы совместимыми
Branded type: `string & { readonly [brand]: "UserId" }`, создаётся только через функцию-парсер
Класс с `private`/`#`-полем: совместимость только с тем же объявлением
Цена: приведение `as` в одном месте, бренд теряется при арифметике и сериализации
Библиотечные варианты: `z.string().brand()`, `Brand` в Effect
Что могут спросить следом
- Почему
unique symbolлучше строкового ключа__brand? - Что будет с брендом после JSON.stringify и обратно?
- Стоит ли брендировать всё подряд?
Разбор
В структурной системе две строки неразличимы, поэтому deleteUser(order.id) компилируется. Самый лёгкий способ сделать номинальность: branded type, пересечение базового типа с фантомным полем, которого нет в рантайме. Лучше ключом сделать declare const brand: unique symbol, тогда объект с таким полем руками не собрать. Значение получает бренд только в функции-парсере или конструкторе с проверкой, это единственное место с as. Альтернатива для объектов: класс с приватным полем, такие экземпляры несовместимы с похожими по форме. Цена: бренд теряется при арифметике над числами и при сериализации, после JSON.parse значение надо снова провести через парсер, а избыточное брендирование раздражает команду. Поэтому брендирую идентификаторы, деньги и проверенные строки (email), а не всё подряд.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.