前置知识: TypeScript

映射类型进阶

00:00
3 min Advanced 2026/6/14

键重映射、模板映射与递归映射类型

概述

映射类型是 TypeScript 中最强大的类型变换工具,它允许基于已有类型创建新类型。TypeScript 4.1 引入了键重映射(Key Remapping)语法,使映射类型能够变换键名而不仅仅是值类型。结合模板字面量类型和条件类型,映射类型可以实现 getter/setter 生成、属性过滤、递归深度操作等高级类型变换。掌握映射类型进阶用法是构建复杂类型系统的关键。

基础概念

键重映射:使用 as 子句在映射类型中变换键名,语法为 { [P in keyof T as NewKey]: T[P] }。这是 TypeScript 4.1 引入的特性。

模板字面量类型:在键重映射中使用模板字符串生成新的键名,如 `get${Capitalize<P>}`,用于自动生成 getter/setter 等方法名。

键过滤:在键重映射的 as 子句中使用条件类型,将不满足条件的键映射为 never,从而在结果类型中移除这些键。

递归映射类型映射类型可以递归引用自身,用于处理嵌套对象类型变换。需要正确处理 Function原始类型以避免无限递归

快速上手

键重映射基础

// 将所有属性名加上前缀
type PrefixKeys<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface User {
  name: string;
  age: number;
}

type PrefixedUser = PrefixKeys<User, 'user'>;
// { userName: string; userAge: number }

Getter 生成

// 为每个属性生成 getter 方法
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface User {
  name: string;
  age: number;
}

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }

// 使用
const getters: UserGetters = {
  getName: () => '张三',
  getAge: () => 30,
};

详细用法

按类型过滤属性

// 只保留指定类型的属性
type FilterByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P];
};

interface Config {
  name: string;
  count: number;
  enabled: boolean;
  items: string[];
}

type StringProps = FilterByType<Config, string>;
// { name: string }

type NumberProps = FilterByType<Config, number>;
// { count: number }

type ArrayProps = FilterByType<Config, readonly any[]>;
// { items: string[] }

按键名模式过滤

// 只保留以特定前缀开头的属性
type FilterByPrefix<T, P extends string> = {
  [K in keyof T as K extends `${P}${string}` ? K : never]: T[K];
};

interface APIResponse {
  userId: number;
  userName: string;
  userRole: string;
  postId: number;
  postTitle: string;
}

type UserFields = FilterByPrefix<APIResponse, 'user'>;
// { userId: number; userName: string; userRole: string }

type PostFields = FilterByPrefix<APIResponse, 'post'>;
// { postId: number; postTitle: string }

双向映射

// 生成 getter 和 setter
type Accessors<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
} & {
  [P in keyof T as `set${Capitalize<string & P>}`]: (value: T[P]) => void;
};

interface Point {
  x: number;
  y: number;
}

type PointAccessors = Accessors<Point>;
// { getX: () => number; getY: () => number; setX: (value: number) => void; setY: (value: number) => void }

递归映射类型

// 深度 Readonly:递归处理嵌套对象
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P];
};

// 深度 Partial:递归处理嵌套对象
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? (T[P] extends Function ? T[P] : DeepPartial<T[P]>) : T[P];
};

// 深度 Required:递归移除所有可选标记
type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepRequired<T[P]>
    : T[P];
};

// 使用示例
interface NestedConfig {
  api: {
    baseURL: string;
    timeout?: number;
    headers: {
      authorization?: string;
      contentType: string;
    };
  };
  features?: {
    darkMode: boolean;
    analytics?: boolean;
  };
}

type PartialConfig = DeepPartial<NestedConfig>;
// 所有层级属性都变为可选

type ReadonlyConfig = DeepReadonly<NestedConfig>;
// 所有层级属性都变为只读

常见场景

事件映射

// 根据事件类型生成事件处理器映射
type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string; code: string };
  focus: {};
  change: { value: string };
};

type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (event: T[K]) => void;
};

type Handlers = EventHandlers<EventMap>;
// {
//   onClick: (event: { x: number; y: number }) => void;
//   onKeydown: (event: { key: string; code: string }) => void;
//   onFocus: (event: {}) => void;
//   onChange: (event: { value: string }) => void;
// }

API 路由类型

// 根据 HTTP 方法生成路由类型
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';

type RouteMap = {
  '/users': { GET: User[]; POST: User };
  '/users/:id': { GET: User; PUT: User; DELETE: void };
};

type RouteHandlers = {
  [Path in keyof RouteMap]: {
    [Method in keyof RouteMap[Path] as Lowercase<string & Method>]: (
      ...args: any[]
    ) => Promise<RouteMap[Path][Method]>;
  };
};

表单字段映射

// 为表单字段生成验证器和错误信息
type FormSchema<T> = {
  [K in keyof T]: {
    value: T[K];
    validate: (value: T[K]) => boolean;
    error?: string;
  };
};

interface LoginForm {
  username: string;
  password: string;
  remember: boolean;
}

type LoginFormSchema = FormSchema<LoginForm>;
// {
//   username: { value: string; validate: (value: string) => boolean; error?: string };
//   password: { value: string; validate: (value: string) => boolean; error?: string };
//   remember: { value: boolean; validate: (value: boolean) => boolean; error?: string };
// }

注意事项

  • 键重映射中的 never:当 as 句的条件结果为 never 时,该键会从结果类型中移除。这是过滤属性的机制
  • Capitalize 的限制Capitalize 内置工具类型只能处理字符串类型。如果键可能是 symbol,需要使用 string & K类型收窄。
  • 递归深度限制TypeScript 递归类型有深限制(约 50 层)。过深的嵌套会导致编译错误于极深的对象结构,考虑使用扁平化方案。
  • Function 过滤递归映射类型中必须过滤 Function 类型,否则函数的 prototype 等属性也会被递归处理。
  • 模板字面量中的联合类型:当模板字面量联合类型时,结果会自动分发联合类型。例如 `a${'x' | 'y'}`生成 'ax' | 'ay'

进阶用法

条件键重映射

// 根据属性类型决定键名变换
type ConditionalRemap<T> = {
  [K in keyof T as T[K] extends Function
    ? K
    : T[K] extends boolean
      ? `is${Capitalize<string & K>}`
      : K]: T[K];
};

interface Model {
  name: string;
  active: boolean;
  visible: boolean;
  save(): void;
}

type Remapped = ConditionalRemap<Model>;
// { name: string; isActive: boolean; isVisible: boolean; save: () => void }

键名解析

// 从键名中解析出前缀和后缀
type ParseKey<K> = K extends `${infer Prefix}_${infer Suffix}`
  ? { prefix: Prefix; suffix: Suffix }
  : { prefix: K; suffix: never };

// 按前缀分组
type GroupByPrefix<T> = {
  [K in keyof T as ParseKey<K>['prefix']]: {
    [P in keyof T as P extends `${ParseKey<K>['prefix']}_${infer Rest}` ? Rest : never]: T[P];
  };
};

interface FlatConfig {
  db_host: string;
  db_port: number;
  api_url: string;
  api_timeout: number;
}

type GroupedConfig = GroupByPrefix<FlatConfig>;
// { db: { host: string; port: number }; api: { url: string; timeout: number } }

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式