Условные и утилитарные типы
Вопрос
Что делают Partial, Pick, Omit, Record, ReturnType, и как устроен свой условный тип?
Короткий ответ
Partial<T>делает все поля необязательными.Required<T>делает их обязательными.Pick<T, K>оставляет перечисленные ключи.Omit<T, K>убирает перечисленные ключи.Record<K, V>строит объект с ключамиKи значениямиV.Exclude<T, U>убирает из union те члены, что совместимы сU.Extract<T, U>оставляет только совместимые.ReturnType<F>иParameters<F>достают типы из функции.
Условный тип выглядит как T extends U ? X : Y. infer объявляет переменную типа внутри ветки extends:
type ElementOf<T> = T extends (infer Item)[] ? Item : neverElementOf<string[]> это string.
Разбор
Partial удобен для патча, но опасен для пропсов компонента: случайно необязательным становится и id, без которого экран не живёт. Точечный Pick<User, 'name' | 'email'> честнее.
keyof T даёт union ключей. Вместе с индексом T[K] получается тип значения. Распределённые условные типы раскрываются по каждому члену union отдельно: ToArray<string | number> при T extends unknown ? T[] : never даёт string[] | number[], а не (string | number)[]. Кортеж из одного параметра, [T] extends [unknown], распределение отключает.
На что смотрят на senior
Свой условный тип пишут, когда он убирает повтор в публичном API. Если им пользуются один раз внутри файла, проще назвать конкретный тип руками.
infer в рекурсии по глубоким объектам легко сделать нечитаемым. На разборе кода плюс получает тот, кто может объяснить тип на примере одного вызова, а не только на синтаксисе.
Template literal types, `on${Capitalize<string>}`, встречаются в обёртках над событиями. Их стоит узнать, но не строить всю модель приложения на строковых шаблонах.
Типичные ошибки
- Размазывать
Partial<Props>на компонент и ловитьundefinedв обязательных полях. - Путать
Excludeдля union иOmitдля ключей объекта. - Ждать, что условный тип исполнится в рантайме. Это стирается при компиляции, в JS его нет.
Задача
Чем Omit<User, 'id'> отличается от Exclude<User, 'id'> для такого типа?
type User = { id: string; name: string }Показать ответ
Omit убирает ключи объекта. Результат { name: string }.
Exclude работает с union, не с ключами. User не union из 'id', поэтому Exclude<User, 'id'> останется User: объект не совместим с строкой 'id' и не выкидывается.
Для union ролей нужен Exclude<Role, 'guest'>, для полей формы — Omit.