映射类型的原理
映射类型的原理
一句话概括
映射类型 = keyof + in + 索引访问——它让类型也拥有了”循环”,一行 [P in keyof T] 就能把对象的每个属性遍历一遍并改造,是 Partial / Pick / Readonly 等内置工具类型的底层引擎。
核心知识点
1. 三件套:keyof、in、T[P]
1
2
3
4
5
6
7
8
9
10
11
// keyof:取出所有键名 → 联合类型
type Keys = keyof { name: string; age: number }; // "name" | "age"
// in:遍历联合类型中的每个成员
type Map = { [K in "x" | "y"]: number }; // { x: number; y: number }
// T[P]:索引访问,取出属性 P 对应的类型
type NameType = User["name"]; // string
// 三者结合 → 映射类型
type Clone<T> = { [P in keyof T]: T[P] };
2. 修饰符操作:? / -? / readonly / -readonly
1
2
3
4
5
6
7
8
// Partial:全部变可选
type MyPartial<T> = { [P in keyof T]?: T[P] };
// Required:移除所有可选(-? 是关键)
type MyRequired<T> = { [P in keyof T]-?: T[P] };
// Mutable:移除 readonly
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
3. as 重映射——过滤和改名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 只保留 string 类型的属性
type StringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed { id: number; name: string; desc: string; }
type OnlyStrings = StringProps<Mixed>; // { name: string; desc: string }
// 给所有键加 get 前缀
type Getters<T> = {
[K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }
as never 的键会被自动从结果中删除——这是过滤属性的核心技巧。
4. 递归映射类型:DeepReadonly / DeepPartial
1
2
3
4
5
6
7
8
9
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function ? T[P] : DeepReadonly<T[P]>
: T[P];
};
interface Config { db: { host: string; port: number } }
type Frozen = DeepReadonly<Config>;
// Frozen.db.host 也是 readonly ✅
5. 映射类型与条件类型组合重映射
1
2
3
4
5
6
7
8
9
10
11
// 按属性名模式过滤:只保留事件处理器
type EventHandlers<T> = {
[K in keyof T as K extends `on${string}` ? K : never]: T[K];
};
interface Component {
onClick: () => void;
onHover: () => void;
disabled: boolean;
}
type Handlers = EventHandlers<Component>; // { onClick: ..., onHover: ... }
「其实你每天都在用」
- **Partial
**:编辑表单,所有字段可选 **Pick<User, “id” “name”>**:列表只展示关键字段 - Omit<Config, “secret”>:返回给前端时排除敏感字段
- Record<string, Rule[]>:表单校验规则字典
- **Readonly
**:全局配置对象防止误改
常见误解(FAQ)
❌ 误区 1:in 可以在值代码里用
不行。[P in keyof T] 中的 in 是类型运算符,只在 type 定义里有效。JavaScript 的 for...in 是另一回事,两者语法相似但作用域完全不同。
❌ 误区 2:映射类型会产生运行时代码
像所有高级类型一样,映射类型是纯编译时机制。Partial<Todo> 编译后消失,等价于你手写了 { title?: string; ... }。
❌ 误区 3:as never 只是”删掉属性”
它本质上是条件类型:不符合条件的键映射到 never,TS 自动从对象类型中抹掉 never 值的键。等价于先过滤再 Pick。
❌ 误区 4:DeepReadonly 会无限递归
不会。TypeScript 的条件类型有递归深度限制(约 50 层),且基本类型(string/number/boolean 等)不会匹配 extends object,形成递归基。
一句话总结
记住这个公式就够了:[P in keyof T]?: T[P] → Partial。? 变可选,-? 变必填,加 as 能过滤/重命名,套个递归就是深度的——所有内置工具类型都是这个模板的变体。
本文由作者按照 CC BY 4.0 进行授权