Что такое mapped types в TypeScript?
[K in keyof T] для итерации по ключам типа.Mapped Types в TypeScript
Mapped types (отображаемые типы) позволяют создавать новые типы на основе существующих, применяя преобразование к каждому свойству исходного типа. Синтаксически они напоминают цикл for...in, но на уровне системы типов.
Базовый синтаксис
Основа mapped type — конструкция [K in keyof T], где K перебирает все ключи типа T:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};
Модификаторы свойств
С помощью + и - можно добавлять или удалять модификаторы readonly и ?:
// Удаляем readonly со всех свойств
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Делаем все свойства обязательными
type Required<T> = {
[K in keyof T]-?: T[K];
};
Remapping ключей через as
TypeScript 4.1 добавил возможность переименовывать ключи с помощью as:
// Добавляем префикс get к каждому свойству
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
Фильтрация свойств через never
Если remapping вернёт never, свойство исключается из результирующего типа:
// Оставляем только строковые свойства
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type User = { name: string; age: number; email: string };
type StringUser = StringProperties<User>;
// { name: string; email: string }
Практические примеры
Mapped types лежат в основе большинства утилитных типов стандартной библиотеки:
// Pick — выбираем подмножество свойств
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Record — создаём объект с заданными ключами и типом значений
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
// Глубокий Readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
Дистрибутивность и union-типы
При итерации по union-типу mapped type обрабатывает каждый элемент отдельно:
type Flags<T extends string> = {
[K in T]: boolean;
};
type FeatureFlags = Flags<'darkMode' | 'beta' | 'premium'>;
// { darkMode: boolean; beta: boolean; premium: boolean }
Связь с Conditional Types
Mapped types часто комбинируются с условными типами для создания мощных преобразований:
// Делаем свойства опциональными только если их тип включает undefined
type SmartPartial<T> = {
[K in keyof T]: undefined extends T[K] ? T[K] : T[K] | undefined;
};
Mapped types — один из ключевых инструментов для создания переиспользуемых абстракций над типами и реализации принципа DRY на уровне системы типов.
Что хочет услышать интервьюер
Понимание синтаксиса `[K in keyof T]` и того, как TypeScript итерирует по ключам типа
Знание модификаторов `+`/`-` для `readonly` и `?`, умение объяснить разницу между добавлением и удалением
Знакомство со стандартными утилитными типами (`Partial`, `Required`, `Readonly`, `Pick`, `Record`) и понимание их реализации через mapped types
Знание remapping через `as` (TypeScript 4.1+) и фильтрации свойств через `never`
Способность показать практический сценарий применения: трансформация API-ответов, построение форм, типизация конфигураций
Пример: Модификаторы +/- в mapped types
// Базовый mapped type — делаем все свойства опциональными
type Partial<T> = {
[K in keyof T]?: T[K];
};
// Удаляем опциональность и readonly
type Concrete<T> = {
-readonly [K in keyof T]-?: T[K];
};
type Config = {
readonly host?: string;
readonly port?: number;
};
type StrictConfig = Concrete<Config>;
// { host: string; port: number } — ни readonly, ни ?
Пример: Remapping ключей через as
// Remapping ключей через as (TypeScript 4.1+)
type EventMap<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserState = {
name: string;
age: number;
loggedIn: boolean;
};
type UserEvents = EventMap<UserState>;
// {
// onName: (value: string) => void;
// onAge: (value: number) => void;
// onLoggedIn: (value: boolean) => void;
// }
Пример: Фильтрация через never
// Фильтрация свойств: оставляем только функции
type MethodsOnly<T> = {
// eslint-disable-next-line @typescript-eslint/ban-types
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
class UserService {
name = 'UserService';
version = 1;
getUser(id: number) { return { id }; }
deleteUser(id: number) { return true; }
}
type ServiceMethods = MethodsOnly<UserService>;
// { getUser: (id: number) => ...; deleteUser: (id: number) => ... }
// name и version отфильтрованы через never
Типичные ошибки
Путают mapped types с index signatures — `{ [key: string]: T }` не является mapped type, так как не итерирует по конкретным ключам
Не знают про модификаторы `-readonly` и `-?`, думая, что нельзя убрать уже установленные модификаторы
Забывают, что remapping через `as` появился только в TypeScript 4.1 — предлагают несуществующий синтаксис для старых версий
Не могут объяснить, как `never` в `as`-клаузе приводит к исключению свойства из типа
Смешивают mapped types с generic-типами, не понимая, что mapped type — это конструкция внутри тела типа, а не сам по себе generic


