映射类型
本教程共 80 篇 · 第 44 篇 · 更新于 2026-08-10 · 约 12 分钟阅读
本节目标:理解映射类型的核心语法
[K in keyof T],学会控制属性的读写修饰符(readonly/?),用as子句做键名变换,并将映射类型与条件类型组合使用。
前面两章讲工具类型时你已经见过映射类型了——Partial、Required、Readonly,全是一行映射类型搞定。映射类型的本质非常直观:把现有类型的每个属性拿出来,挨个换个马甲,再拼成一个新类型。
这一章我们从最基础的语法开始,逐步讲到键名变换和高级组合。
基础语法:遍历属性再改造
映射类型的模板:
type NewType<T> = {
[K in keyof T]: /* 新类型,可以用 T[K] 引用原类型 */
};
最简示例——把对象类型的所有属性值变成 boolean:
interface Features {
darkMode: () => void;
newUserProfile: () => void;
autoSave: () => void;
}
type FeatureFlags = {
[K in keyof Features]: boolean;
};
// FeatureFlags 等价于:
// {
// darkMode: boolean;
// newUserProfile: boolean;
// autoSave: boolean;
// }
[K in keyof Features] 遍历了 Features 的每一个键名("darkMode" | "newUserProfile" | "autoSave"),每个属性的值类型都是 boolean。原来的值类型(函数)被完全替换掉了。
再进一步——保留原值类型,但全部包一层数组:
type Arrayify<T> = {
[K in keyof T]: T[K][];
};
interface Person {
name: string;
age: number;
}
type PersonArrays = Arrayify<Person>;
// {
// name: string[];
// age: number[];
// }
T[K] 引用的是原类型中键 K 对应的值类型——这是索引访问类型,在映射类型里特别常用。
修饰符:+ 和 - 控制 readonly 和 ?
映射类型有两个修饰符:readonly 和 ?。你可以用 + 加上、用 - 去掉。省略前缀时默认是 +。
加 readonly(+readonly / readonly)
type Lock<T> = {
readonly [K in keyof T]: T[K];
};
interface Config {
host: string;
port: number;
}
const config: Lock<Config> = {
host: "localhost",
port: 3000,
};
// config.host = "other"; // ❌ 只读
去掉 readonly(-readonly)
type Unlock<T> = {
-readonly [K in keyof T]: T[K];
};
interface Immutable {
readonly id: number;
readonly name: string;
}
type Mutable = Unlock<Immutable>;
// { id: number; name: string; } —— readonly 全被移除了
去掉可选标记(-?)
// Required 的内核就是这一行
type ForceRequired<T> = {
[K in keyof T]-?: T[K];
};
interface LooseBag {
name?: string;
count?: number;
}
type TightBag = ForceRequired<LooseBag>;
// { name: string; count: number; }
修饰符组合
修饰符可以同时用:
// 全部变只读 + 全部变可选
type ReadonlyPartial<T> = {
readonly [K in keyof T]?: T[K];
};
键名重映射:用 as 给键换名字
TypeScript 4.1 引入的 as 子句,让你在映射的过程中改变键名:
type Rename<T> = {
[K in keyof T as `new_${string & K}`]: T[K];
};
interface User {
name: string;
age: number;
}
type RenamedUser = Rename<User>;
// {
// new_name: string;
// new_age: number;
// }
as 后面可以放任何有效的类型表达式。最经典的用法是组合模板字面量类型做前缀/后缀变换:
// 给每个属性生成对应的 getter 方法
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// }
Capitalize<string & K> 把 K 的首字母大写——"name" 变成 "Name",然后拼接 "get"。string & K 的交集操作是为了让 TypeScript 确认 K 确实是 string 子类型(keyof 可能返回 symbol)。
用 as 过滤键名
as 的另一个妙用:把键映射为 never,就能在映射过程中过滤掉某些属性:
// 去掉 Type 中名为 "kind" 的属性
type RemoveKind<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKind<Circle>;
// { radius: number; } ——kind 被过滤掉了
原理回顾:Exclude<K, "kind"> 对 "kind" | "radius" 做一个条件判断,"kind" 自己被映射为 never,never 作为键名时属性会被自动移除。
对联合类型做映射
as 还有一个典型场景——遍历的不是 keyof T,而是一个普通的联合类型:
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
};
type SquareEvent = { kind: "square"; x: number; y: number };
type CircleEvent = { kind: "circle"; radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>;
// {
// square: (event: SquareEvent) => void;
// circle: (event: CircleEvent) => void;
// }
遍历 Events 联合的每个成员 E,然后用 E["kind"] 作为键名——这就是”把联合类型里每个成员的 kind 字段提取为键”。
映射类型与条件类型组合
工具库里的大部分类型变换都是映射类型和条件类型的组合。比如”只保留函数类型的属性”:
// 只保留值类型是函数的属性
type FunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
greet: (name: string) => string;
calculate: (x: number, y: number) => number;
}
type FuncOnly = FunctionProperties<Mixed>;
// {
// greet: (name: string) => string;
// calculate: (x: number, y: number) => number;
// }
这里 as 子句里嵌了一个条件类型:T[K] extends Function ? K : never。
- 如果属性
K的值类型是函数,K原样保留; - 否则,
K变成never,该属性被过滤掉。
同样的思路可以反过来——只保留非函数属性:
type NonFunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
type DataOnly = NonFunctionProperties<Mixed>;
// { name: string; age: number; }
值类型上的条件判断
条件类型不一定非要放在 as 里——你也可以用它来变换值类型:
// 把值为 null 的属性变成 undefined,其他保持不变
type NullToUndefined<T> = {
[K in keyof T]: T[K] extends null ? undefined : T[K];
};
interface Data {
a: string;
b: null;
c: number | null;
}
type Cleaned = NullToUndefined<Data>;
// {
// a: string;
// b: undefined;
// c: number | undefined; ——null 被替换为 undefined
// }
注意 c 的结果是 number | undefined——因为 number | null extends null 这个条件是 false(联合类型不是单个 null),所以 c 的值类型保持原样。
小结
映射类型是 TypeScript 类型系统的”批量处理”工具。核心公式:
{ [K in 键来源 as 键变换]: 值变换 }
其中:
- 键来源:通常是
keyof T或某个联合类型。 - 键变换(可选):用
as做键名变换,或者映射为never实现过滤。 - 值变换:可以是简单的
T[K]、条件类型、或者任何合法的类型表达式。
映射类型 + 条件类型 = TypeScript 的”类型编程”基础。内置的 Partial、Required、Pick、Omit 全是这么实现的。理解了这一章的语法,你就有了自己动手写工具类型的能力——从”消费型开发”升级为”基础设施型开发”。