TypeScript / 39 минут
Mapped и conditional types
Стройте связанные типы с mapped, conditional, infer и шаблонными литералами.
- Стройте связанные типы с mapped, conditional, infer и шаблонными литералами.
- Применить тему в задаче «Mapped и conditional types» и объяснить граничные случаи.
Около 9 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Преобразование типа по его ключам
Mapped type строит новую объектную форму, перебирая ключи исходного типа на уровне компилятора. Например, можно превратить набор числовых настроек в набор boolean-флагов с теми же именами. Это не runtime-цикл: после компиляции никакой объект от одной записи mapped type не появится. Реальные значения нужно создать обычным JavaScript-кодом.
Ключевая польза — сохранение связи между моделями. Если в исходную форму добавлен ключ, производная форма тоже учитывает его по заданному правилу. Это уменьшает ручное дублирование и помогает обнаружить пропущенное поле. Но чрезмерно сложное преобразование может стать труднее поддержки, чем небольшой явно записанный интерфейс.
type Flags<T> = { [K in keyof T]: boolean };
type FeatureValues = { sound: number; animation: number };
type FeatureFlags = Flags<FeatureValues>;
const flags: FeatureFlags = { sound: true, animation: false };Модификаторы optional и readonly
Mapped type может добавлять или снимать ? и readonly. Так устроены многие стандартные utility types. Это позволяет описать, например, обязательную форму после проверки или доступное только для чтения представление. Снятие optional не проводит валидацию: оно лишь создаёт тип, использовать который честно можно после реального обеспечения нужных свойств.
type Concrete<T> = { [K in keyof T]-?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };Не используйте Mutable как автоматическое разрешение менять общий объект, который другие части считают неизменяемым. Типовая операция может изменить доступные компилятору права, но не меняет смысл владения данными. Иногда правильнее создать новую копию или отдельный builder, чем снимать ограничения с чужой структуры.
Переименование ключей и шаблонные литералы
Через as в mapped type можно преобразовать или отфильтровать ключ. Template literal types позволяют строить наборы строковых имён из других типов. Это полезно для событий, методов доступа и связанных API. Обычно нужно ограничить ключи подходящим строковым набором, потому что keyof может содержать не только строки.
type Events<T> = {
[K in keyof T & string as `${K}Changed`]: (value: T[K]) => void;
};
type ProfileEvents = Events<{ name: string; age: number }>;Здесь nameChanged принимает string, а ageChanged — number. Связь полезна, пока runtime-реализация действительно отправляет эти события с правильными данными. Объявить красивый тип недостаточно: генерация имён и проверка внешних payload должны соответствовать заявленному контракту.
Conditional types и infer
Условный тип T extends U ? A : B выбирает форму по отношению типов. Это не if над реальным значением. infer позволяет назвать часть сопоставленного типа и использовать её в успешной ветви. Например, из Promise можно извлечь результат или из функции — возвращаемый тип. Для повседневных случаев сначала проверяйте стандартные Awaited и ReturnType, а не создавайте собственный сложный механизм.
type ElementOf<T> = T extends readonly (infer Item)[] ? Item : never;
type NumberItem = ElementOf<number[]>; // number
type NotArray = ElementOf<string>; // neverНекорректное предположение об infer может сделать тип слишком широким или превратить важный случай в never. Проверяйте преобразование на нескольких маленьких примерах, включая отсутствие совпадения. Сообщение компилятора часто легче понять после выделения промежуточного type alias.
Распределение по union и читаемость
Если проверяемая сторона условного типа является голым параметром типа, условие обычно распределяется по вариантам union. Это удобно для фильтрации, но иногда неожиданно для автора. Оборачивание в кортежи позволяет рассмотреть union целиком. Разница должна быть сознательным выбором, а не побочным эффектом случайной записи.
type ToArray<T> = T extends unknown ? T[] : never;
type Separate = ToArray<string | number>; // string[] | number[]
type Together<T> = [T] extends [unknown] ? T[] : never;
type Mixed = Together<string | number>; // (string | number)[]Не измеряйте качество типов количеством вложенных условных выражений. Публичный тип должен помогать потребителю. Если сообщение об ошибке становится непроходимым, полезно разделить преобразование на именованные стадии или выбрать более явный контракт. Это такая же работа над читаемостью, как выделение функций в обычном коде.
Практика флагов
В задаче mapped type описывает boolean-результат для ключей числового settings. Значение true соответствует строго положительному числу; ноль и отрицательные дают false. Само преобразование объекта нужно реализовать через обход ключей и запись результата. Если вернуть входной settings с утверждением типа, числа в runtime не превратятся в boolean.
Проверьте пустой объект, положительные, нулевые и отрицательные значения. Затем замените входной тип на фиксированную форму и посмотрите, как сохраняется набор имён. Для самостоятельного продолжения опишите тип событий изменения настроек и отдельную runtime-функцию отправки. Явно разделите вычисление значений, проверку типов и валидацию данных на внешней границе.