Mapped types: преобразование ключей и свойств исходной модели
Автор: Казачкин Даниил Михайлович · Обновлено
Mapped type строит объектный тип по набору ключей. Такой механизм полезен, когда рядом с исходной моделью нужны ошибки полей, таблица подписей или набор читающих функций, а…
Mapped type строит объектный тип по набору ключей. Такой механизм полезен, когда рядом с исходной моделью нужны ошибки полей, таблица подписей или набор читающих функций, а ручное повторение каждого свойства создаёт риск рассинхронизации.
Цель и необходимые знания
Вы уже знакомы с generics, Pick, Partial и литеральными типами. Теперь разберём, как keyof, индексный доступ и конструкция [K in ...] работают вместе. Цель — уметь выразить повторяющееся правило преобразования, а не переписать все типы приложения сложным синтаксисом. Пример можно проверить в TypeScript 6 с strict.
От полей формы к ошибкам
У формы занятия есть название, длительность и признак публикации. Для каждого поля может существовать массив ошибок. Если вручную объявить второй объект с теми же тремя ключами, добавление поля формы потребует ещё одного изменения. Вместо этого опишем правило: для каждого ключа исходного типа допускается свойство со списком строк. Затем построим таблицу функций чтения, сохраняя тип значения каждого конкретного поля.
export {};
interface LessonForm {
title: string;
minutes: number;
published: boolean;
}
type FieldErrors<T> = { [K in keyof T]?: string[] };
type FieldReaders<T> = {
[K in keyof T as `read${Capitalize<K & string>}`]: () => T[K];
};
const form: LessonForm = { title: 'Типы', minutes: 80, published: false };
const errors: FieldErrors<LessonForm> = { minutes: ['Длительность нужно согласовать'] };
const readers: FieldReaders<LessonForm> = {
readTitle: () => form.title,
readMinutes: () => form.minutes,
readPublished: () => form.published,
};
console.log(readers.readTitle().toUpperCase());
console.log(readers.readMinutes() + 10);
console.log(errors.minutes?.join('; '));
if (false) {
// @ts-expect-error В исходной форме нет поля location.
const wrong: FieldErrors<LessonForm> = { location: ['Нет аудитории'] };
void wrong;
}Ожидаемый вывод — ТИПЫ, 90, затем текст ошибки длительности. Функции действительно созданы обычным объектом JavaScript. Mapped type не сгенерировал реализацию readTitle; он проверил наличие и сигнатуру нужного свойства. Если изменить тип minutes, возвращаемый тип соответствующей читающей функции автоматически последует за ним.
Прочтём выражение по частям
keyof LessonForm даёт объединение известных ключей. Переменная типа K последовательно представляет один из этих ключей при построении результата. T[K] извлекает тип соответствующего значения. В первом преобразовании нас интересуют только имена, поэтому все значения становятся string[]; во втором важно сохранить связь конкретного имени с исходным типом свойства.
Часть as внутри mapped type переименовывает ключ. В примере к имени добавляется префикс, а первая буква меняется через встроенный Capitalize. Пересечение K & string ограничивает преобразование строковыми ключами: у объектов в общем случае бывают также числовые и символьные ключи. Такой фильтр осмыслен для выбранной формы, но не означает, что символьные свойства всегда нужно молча исключать из любой модели.
Модификаторы и сохранение смысла
Mapped types позволяют добавлять и снимать readonly и необязательность. Запись -readonly снимает ограничение записи в результирующем типе, а -? делает соответствующие свойства обязательными. Эти операции не заполняют пропущенные данные и не меняют свойства уже созданного объекта во время выполнения. Если исходное поле отсутствует, обязательная модель должна быть обеспечена реальным кодом формирования значения.
Необязательно переписывать встроенные utility types в каждом проекте. Их полезно разобрать один раз, чтобы читать декларации библиотек и создавать собственные преобразования там, где готового решения действительно нет. Например, таблица ошибок, возвращаемых вашим редактором, может иметь предметное имя и дополнительно хранить общую ошибку формы. Тогда не стоит искусственно выдавать служебное поле общей ошибки за ключ исходной записи.
Фильтрация ключей и границы преобразования
Если нужен результат без одного свойства, есть готовый Omit. В собственном преобразовании можно исключить ключ через переименование в never. Но фильтрация по типу значения требует ещё и условного типа: например, оставить только строковые поля. Прежде чем объединять механизмы, проверьте каждый отдельно на модели с двумя-тремя свойствами, включая необязательное поле. Иначе сложно понять, на каком шаге исчез нужный ключ.
Для типизации таблицы обработчиков иногда нужно иное направление связи: ключ указывает событие, а значение — функцию с соответствующими данными. Универсальный Record<string, Function> уничтожит эту зависимость. Mapped type способен сохранить её, если исходная модель действительно связывает имена с конкретными типами. При этом реализацию динамического цикла по объекту всё равно нужно проверять отдельно; типовая запись сама по себе не доказывает корректность любого алгоритма обхода.
Типичные ошибки
Не путайте [K in keyof T] с индексной сигнатурой [key: string]. Первая конструкция перебирает известный набор, вторая допускает произвольные строковые ключи. Не ждите, что преобразование типа удалит или переименует реальные поля. Не добавляйте рекурсивное преобразование вложенных объектов без определения поведения для массивов, функций и специальных объектов: универсальность такого помощника часто оказывается лишь кажущейся.
Практикум: обязательные подписи и выборочное редактирование
Добавьте в форму поле level: 'start' | 'advanced'. Создайте тип таблицы подписей, где каждому полю формы обязательно соответствует строка, и убедитесь, что отсутствие подписи level вызывает ошибку. Затем опишите отдельный тип ошибок только для редактируемых полей title, minutes и level. Поле published в этой версии формы меняет отдельная операция.
Разбор: таблицу подписей можно представить как отображение по keyof LessonForm со снятой необязательностью, если она есть у исходных полей. Для выбранного поднабора удобно сначала применить Pick, затем передать результат в FieldErrors. Такая композиция прямо отражает договор редактора. Проверьте добавление нового поля и осознанно решите, должно ли оно автоматически появиться в таблице подписей и в наборе редактирования: у этих двух производных моделей могут быть разные правила развития.
Частые вопросы
Mapped type заменяет Object.fromEntries?
Нет. Mapped type строит описание для компилятора, а Object.fromEntries создаёт объект во время выполнения. Они могут использоваться в одном решении, но корректный тип не гарантирует, что переданные пары действительно содержат все необходимые ключи.
Почему шаблон имени метода использует K & string?
Исходный набор ключей может включать не только строки. Операции над строковыми литералами требуют строкового аргумента. Ограничение объясняет это компилятору и одновременно фиксирует правило преобразования: в данном помощнике методы строятся только для строковых имён.
Связанные исследования
- Utility types как преобразования моделей: что доказывают Pick, Omit, Partial и Record — Проверяем границы преобразований моделей: почему Omit не удаляет данные, Partial меняет состояния команды, Record зависит от ключей, а union может потерять связи.