TypeScriptMiddleCoding
Что такое mapped types и как создать пользовательский mapped type?
Mapped type создаёт новый тип итерацией по ключам другого: { [K in keyof T]: ... }. Поддерживает модификаторы readonly/?, их удаление через минус, и переименование ключей через as с шаблонными литералами.
Что такое mapped types
Mapped type — это способ создать новый тип, итерируясь по ключам другого типа и трансформируя их. Синтаксис: { [K in SomeKeys]: TransformedType }. Это механизм, лежащий в основе большинства встроенных утилитарных типов TypeScript.
Базовый синтаксис
type Flags<T> = {
[K in keyof T]: boolean;
};
interface User {
id: number;
name: string;
email: string;
}
type UserFlags = Flags<User>;
// { id: boolean; name: boolean; email: boolean }
Модификаторы: readonly и ?
Знаки + и - добавляют или убирают модификаторы у каждого ключа:
// Добавить readonly (+ по умолчанию)
type Immutable<T> = {
readonly [K in keyof T]: T[K];
};
// Убрать readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Убрать опциональность (аналог Required)
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
Переименование ключей через as (remapping)
С TypeScript 4.1 можно переименовывать ключи с помощью as:
// Добавить префикс get к каждому ключу
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }
// Отфильтровать ключи: remapping в never удаляет ключ
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringUserFields = OnlyStrings<User>;
// { name: string; email: string }
Пользовательский mapped type: DeepReadonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
interface Config {
server: {
host: string;
port: number;
};
debug: boolean;
}
type ImmutableConfig = DeepReadonly<Config>;
// config.server.host — только для чтения на любой глубине
Mapped type по произвольному union
В качестве источника ключей можно использовать любой string/number literal union, не только keyof T:
type HttpMethods = "GET" | "POST" | "PUT" | "DELETE";
type MethodHandlers = {
[M in HttpMethods]: (url: string, body?: unknown) => Promise<Response>;
};
const client: MethodHandlers = {
GET: (url) => fetch(url),
POST: (url, body) => fetch(url, { method: "POST", body: JSON.stringify(body) }),
PUT: (url, body) => fetch(url, { method: "PUT", body: JSON.stringify(body) }),
DELETE: (url) => fetch(url, { method: "DELETE" }),
};
Встроенные утилиты, реализованные через mapped types
Partial<T>— все поля опциональныRequired<T>— все поля обязательныReadonly<T>— все поля только для чтенияRecord<K, V>— объект с ключами K и значениями VPick<T, K>— подмножество полейOmit<T, K>— исключение полей
Подводные камни
- Remapping ключа в
neverудаляет его из типа — полезно, но легко допустить ошибку в условии и потерять нужные поля. - Mapped types работают поверхностно по умолчанию: вложенные объекты не трансформируются автоматически, нужна явная рекурсия.
- Рекурсивные mapped types могут вызвать ошибку «Type alias circularly references itself» — обходите через условный тип или
interface. - При использовании
Capitalizeв as-remapping ключ должен бытьstring & K, а не простоK, иначе компилятор жалуется на символьные ключи. Record<string, V>и mapped type поstring— разные вещи по поведению с index signature; не всегда взаимозаменяемы.- Модификатор
-?(убрать опциональность) не делает тип non-nullable — поля могут по-прежнему бытьundefined, если это часть их типа. - Большие mapped types с ремаппингом замедляют автодополнение; разбивайте на промежуточные алиасы.
Common mistakes
- Смешивать «mapped types» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «mapped types» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет преобразование свойств типа по ключам.
- Показывает на примере, как работает: mapped type проходит по
keyofисходного типа и создает новую объектную форму, при необходимости меняя optional, readonly или имена ключей. - Называет production-нюанс и граничный случай для темы «mapped types».