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 типов

Предпосылки

Базовый синтаксис

// { [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"]>;
};

Частые ошибки

  1. Забыть 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] };
  1. Рекурсия без выхода — DeepPartial для циклических типов может зависнуть
  2. Путать in keyof T и in Tkeyof T для итерации по ключам объекта, T для union
  3. Потеря модификаторов — mapped type по умолчанию сохраняет readonly/?, но ремаппинг as их сбрасывает

Практика

  1. Реализуйте Partial<T>, Required<T>, Readonly<T> вручную
  2. Создайте Getters<T> — преобразование свойств в getter-методы
  3. Напишите PickByType<T, V> — выбрать свойства по типу значения
  4. Реализуйте DeepReadonly<T> — рекурсивный readonly
  5. Создайте mapped type для генерации event handlers из интерфейса состояния

Связанные темы

Ресурсы