Mapped Types
Mapped types создают новые типы путём трансформации каждого свойства существующего типа:
{ [K in keyof T]: NewType }.
Зачем нужно
- Трансформация объектных типов: сделать все поля readonly, опциональными, обязательными
- Создание производных типов без дублирования
- Основа для
Partial,Required,Readonly,Recordи других utility types - Переименование ключей через
as
Где используется
- Все встроенные utility types для объектов (Partial, Required, Readonly, Pick, Record)
- Трансформация API-типов
- Создание типов форм из моделей данных
- Генерация getter/setter типов
Предпосылки
- typeof и keyof — keyof для итерации по ключам
- Generics — параметрические типы
- Conditional types — для условных mapped types
Базовый синтаксис
// { [K in Keys]: ValueType }
// K — переменная итерации
// Keys — union строковых литералов для итерации
// ValueType — тип значения для каждого ключа
// Простой пример: объект из union ключей
type Flags = { [K in "option1" | "option2" | "option3"]: boolean };
// { option1: boolean; option2: boolean; option3: boolean }
// Через keyof — итерация по ключам существующего типа
type ReadonlyUser = { [K in keyof User]: Readonly<User[K]> };
Mapped type с keyof T
interface User {
id: number;
name: string;
email: string;
}
// Все свойства опциональные (аналог Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Все свойства readonly (аналог Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Все свойства обязательные (аналог Required<T>)
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Все значения — string
type Stringify<T> = {
[K in keyof T]: string;
};
type StringifiedUser = Stringify<User>;
// { id: string; name: string; email: string }
Модификаторы: +/- readonly и +/- ?
// + добавляет модификатор (по умолчанию)
// - убирает модификатор
// Добавить readonly
type AddReadonly<T> = {
+readonly [K in keyof T]: T[K]; // + можно опустить
};
// Убрать readonly
type RemoveReadonly<T> = {
-readonly [K in keyof T]: T[K];
};
interface FrozenUser {
readonly id: number;
readonly name: string;
}
type MutableUser = RemoveReadonly<FrozenUser>;
// { id: number; name: string } — без readonly
// Добавить опциональность
type AddOptional<T> = {
[K in keyof T]+?: T[K]; // + можно опустить
};
// Убрать опциональность
type RemoveOptional<T> = {
[K in keyof T]-?: T[K];
};
interface PartialConfig {
host?: string;
port?: number;
}
type FullConfig = RemoveOptional<PartialConfig>;
// { host: string; port: number }
Key Remapping с as (TypeScript 4.1+)
Переименование ключей в mapped type:
// Базовый ремаппинг
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// {
// getName: => string;
// getAge: => number;
// }
// Setters
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserSetters = Setters<User>;
// {
// setName: (value: string) => void;
// setAge: (value: number) => void;
// }
Фильтрация ключей через as + never
// Оставить только строковые свойства
type OnlyStringValues<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface User {
id: number;
name: string;
email: string;
age: number;
}
type StringProps = OnlyStringValues<User>;
// { name: string; email: string }
// Убрать определённые ключи (аналог Omit)
type MyOmit<T, Keys extends keyof T> = {
[K in keyof T as K extends Keys ? never : K]: T[K];
};
type WithoutId = MyOmit<User, "id">;
// { name: string; email: string; age: number }
// Оставить только методы
type MethodsOnly<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
Event Map из типа
type EventMap<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
newValue: T[K],
oldValue: T[K]
) => void;
};
interface State {
count: number;
name: string;
active: boolean;
}
type StateEvents = EventMap<State>;
// {
// onCountChange: (newValue: number, oldValue: number) => void;
// onNameChange: (newValue: string, oldValue: string) => void;
// onActiveChange: (newValue: boolean, oldValue: boolean) => void;
// }
Комбинирование с Conditional Types
// Сделать все строковые поля nullable, остальные не трогать
type NullableStrings<T> = {
[K in keyof T]: T[K] extends string ? T[K] | null : T[K];
};
interface User {
id: number;
name: string;
email: string;
age: number;
}
type Result = NullableStrings<User>;
// {
// id: number;
// name: string | null;
// email: string | null;
// age: number;
// }
// Глубокий Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends any
? T[K]
: DeepPartial<T[K]>
: T[K];
};
// Глубокий Readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
Реализация встроенных utility types
// Partial<T>
type Partial<T> = { [K in keyof T]?: T[K] };
// Required<T>
type Required<T> = { [K in keyof T]-?: T[K] };
// Readonly<T>
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Pick<T, K>
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Record<K, V>
type Record<K extends keyof any, V> = { [P in K]: V };
// Omit<T, K> (через Pick + Exclude)
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Продвинутые примеры
Form Validation Types
type ValidationRule<T> = {
required?: boolean;
min?: T extends number ? number : never;
max?: T extends number ? number : never;
minLength?: T extends string ? number : never;
maxLength?: T extends string ? number : never;
pattern?: T extends string ? RegExp : never;
};
type FormValidation<T> = {
[K in keyof T]?: ValidationRule<T[K]>;
};
interface UserForm {
name: string;
age: number;
email: string;
}
const validation: FormValidation<UserForm> = {
name: { required: true, minLength: 2, maxLength: 50 },
age: { required: true, min: 0, max: 150 },
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
};
API Client из описания маршрутов
interface ApiRoutes {
"/users": { response: User; params: never };
"/users/:id": { response: User; params: { id: string } };
"/posts": { response: Post; params: never };
}
type ApiClient = {
[Path in keyof ApiRoutes as `fetch${Capitalize<
Path extends `/${infer Name}` ? Name : string
>}`]: ApiRoutes[Path]["params"] extends never
? => Promise<ApiRoutes[Path]["response"]>
: (params: ApiRoutes[Path]["params"]) => Promise<ApiRoutes[Path]["response"]>;
};
Частые ошибки
- Забыть
string &при ремаппинге с Capitalize — keyof T может включать symbol
// Ошибка: Type 'K' does not satisfy 'string'
type Bad<T> = { [K in keyof T as `get${Capitalize<K>}`]: T[K] };
// Правильно:
type Good<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: T[K] };
- Рекурсия без выхода — DeepPartial для циклических типов может зависнуть
- Путать
in keyof Tиin T—keyof Tдля итерации по ключам объекта,Tдля union - Потеря модификаторов — mapped type по умолчанию сохраняет readonly/?, но ремаппинг
asих сбрасывает
Практика
- Реализуйте
Partial<T>,Required<T>,Readonly<T>вручную - Создайте
Getters<T>— преобразование свойств в getter-методы - Напишите
PickByType<T, V>— выбрать свойства по типу значения - Реализуйте
DeepReadonly<T>— рекурсивный readonly - Создайте mapped type для генерации event handlers из интерфейса состояния
Связанные темы
- typeof и keyof — keyof для итерации
- Conditional types — условная логика в mapped types
- Template literal types — ремаппинг ключей
- Utility types — встроенные mapped types