映射类型进阶
00:00
键重映射、模板映射与递归映射类型
概述
映射类型是 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 } }