讲解
映射类型(mapped types)是「遍历一个类型的每个属性,逐个变换」的类型级 for 循环,语法是 { [K in keyof T]: T[K] }。内置的 Partial、Readonly 全是它的应用:Partial 展开就是 { [K in keyof T]?: T[K] }——遍历每个键 K,属性名后加 ? 变可选。理解了这一行,所有内置结构变换的实现就都透明了。
映射类型的修饰符可以增也可以减:加 ?/-?(可选)、+readonly/-readonly(只读)。-? 和 -readonly 分别实现了 Required 和「取消只读」。键本身也能变换:as 子句重映射键名,{ [K in keyof T as get${Capitalize<string & K>}]: () => T[K] } 能从数据类型批量派生 getter 接口——这里用了模板字面量类型,把字符串类型当模板拼接,是 TypeScript 4.1 之后的强力特性。
实战建议:映射类型适合造「团队级工具」,比如把 API 响应里的 snake_case 键批量转 camelCase、把表单模型派生成「每字段一个错误信息」的类型。写之前先问:内置类型组合能不能解决?能就不造新的。自定义映射类型务必配注释说明输入输出。
示例
手写 Partial / Readonly 看看原理:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
interface Config {
host: string;
port: number;
}
const patch: MyPartial<Config> = { port: 8080 }; // 只改一项也合法
const frozen: MyReadonly<Config> = { host: "localhost", port: 80 };
// frozen.port = 81; // 若取消注释:readonly,编译报错
console.log(patch.port, frozen.host);
减修饰符:把可选和只读去掉:
type Concrete<T> = { [K in keyof T]-?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
interface Draft {
title?: string;
body?: string;
}
const final: Concrete<Draft> = { title: "定稿", body: "正文" };
const frozen: readonly number[] = [1, 2, 3];
const mutable: Mutable<typeof frozen> = [1, 2, 3];
mutable[0] = 9; // Mutable 去掉了 readonly,可以改
console.log(final.title, mutable[0]);
键重映射:批量派生 getter:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface State {
count: number;
label: string;
}
const stateGetters: Getters<State> = {
getCount: () => 42,
getLabel: () => "计数器",
};
console.log(stateGetters.getCount(), stateGetters.getLabel());
常见坑
- keyof T 里混入 symbol/number 键:键重映射时用 string & K 收窄到字符串键,否则模板拼接报错。
- 忘记 as const 导致键不精确:对普通对象做键重映射,键是 string 而非字面量,派生结果过宽。
- 在映射类型里做运行时假设:类型变换不产生运行时代码——Getters 的对象还是要自己实现,类型只是契约。
- 过度嵌套:三层以上的映射类型组合几乎无法维护,拆成有名字的中间类型,每步一个别名。
小结
映射类型是类型级的属性遍历,?/readonly 可增可减,as 子句重映射键名;内置工具类型都是它的特例。下一章讲类型里的 if:条件类型与 infer。