首页 / TypeScript 入门教程 / 映射类型

TypeScript 入门教程

映射类型

本教程共 80 篇 · 第 44 篇 · 更新于 2026-08-10 · 约 12 分钟阅读

TypeScriptTypeScript 入门教程映射类型泛型

本节目标:理解映射类型的核心语法 [K in keyof T],学会控制属性的读写修饰符(readonly/?),用 as 子句做键名变换,并将映射类型与条件类型组合使用。

前面两章讲工具类型时你已经见过映射类型了——PartialRequiredReadonly,全是一行映射类型搞定。映射类型的本质非常直观:把现有类型的每个属性拿出来,挨个换个马甲,再拼成一个新类型。

这一章我们从最基础的语法开始,逐步讲到键名变换和高级组合。

基础语法:遍历属性再改造

映射类型的模板:

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" 自己被映射为 nevernever 作为键名时属性会被自动移除。

对联合类型做映射

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 的”类型编程”基础。内置的 PartialRequiredPickOmit 全是这么实现的。理解了这一章的语法,你就有了自己动手写工具类型的能力——从”消费型开发”升级为”基础设施型开发”。