type, interface и литералы
Вопрос
Когда брать interface, когда type, и почему строковый union чаще лучше enum?
Короткий ответ
Оба описывают форму объекта. interface можно дополнять через declaration merging: второе объявление с тем же именем добавляет поля. Так расширяют типы библиотек. type умеет union, intersection, кортежи и условные типы. Для пропсов компонента и моделей API разницы почти нет, в одном проекте выбирают один стиль и не мешают без причины.
enum компилируется в объект в рантайме. Строковый union type Role = 'admin' | 'user' исчезает при сборке и сужается через ===. Числовой enum ещё и даёт обратное отображение, его легко перепутать.
as const фиксирует литералы: массив становится readonly-кортежем, поле 'admin' остаётся литералом, а не string. Без этого keyof и аргументы функций размываются до широкого типа.
Разбор
Структурная типизация: объект подходит под тип, если у него есть нужные поля. Явно писать implements не обязательно. Лишнее поле мешает, когда объект передают в объектный литерал напрямую. Если значение уже лежит в переменной, лишние поля TypeScript пропускает.
readonly запрещает присваивание поля в типе. Это не Object.freeze: в рантайме поле всё ещё можно изменить из JS. Для массива readonly string[] нельзя вызвать push.
never — тип, значений у которого нет. Им помечают ветку, которая не должна выполниться, и функции, которые всегда бросают ошибку. Пустой union после Exclude тоже схлопывается в never.
const roles = ['admin', 'user'] as const
type Role = (typeof roles)[number] // 'admin' | 'user'typeof здесь берёт тип значения, не JS-оператор. Индекс [number] достаёт union элементов кортежа.
На что смотрят на senior
interface с merging удобен для declare module. В коде фичи merging случайно склеивает два разных User из разных файлов, если имя совпало и оба глобальные. Локальные типы этому не подвержены.
Перегрузки функций (overload) описывают несколько контрактов одной реализации. Их берут, когда union на входе и выходе связан парами, а не «любой вход даёт любой выход». Если хватает одного дженерика, перегрузка только шумит.
Типичные ошибки
- Ставить
enumтам, где значение уходит в JSON и сравнивается со строкой с бэкенда. - Аннотировать константу как
stringи терять литерал, который нужен дляswitch. - Считать
readonlyзащитой в рантайме. - Путать оператор
typeofв JS иtypeofв позиции типа.
Задача
Что за тип у Role и почему аннотация string[] его ломает?
const roles = ['admin', 'user'] as const
type Role = (typeof roles)[number]Показать ответ
as const делает readonly-кортеж литералов. typeof roles — тип значения, [number] достаёт элемент. Role это 'admin' | 'user'.
Аннотация const roles: string[] стирает литералы до string. Тогда Role станет string, и switch по ролям перестанет быть исчерпываемым.