Menu

TypeScript Record 类型:Record<K, V> 用法详解与示例

Record<K, V> 是键为 K、所有值都为 V 的对象类型。本文讲解用 string 作键和用联合类型作键(每个键都必须存在)的 Record、Partial<Record>、以带类型的键遍历 Record、缺失键的陷阱,以及什么时候该改用索引签名或 Map。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Record<K, V> 是一个内置工具类型,表示键的类型为 K、所有值的类型都为 V 的对象。用 string 作键时,它描述的是一个字典;用字面量联合类型作键时,它描述的是一个必须恰好拥有这些键的对象。

Record 只存在于类型系统中。在运行时,这两个对象都是普通的 JavaScript 对象,所以对象字面量、展开运算符、JSON.stringify 以及其他任何接受对象的地方都能用它们。

语法与定义

Record<Keys, Value>

Keys 必须是可以作为对象键的类型:string、number、symbol、字符串或数字字面量的联合类型,或者模板字面量类型。Value 可以是任意类型。TypeScript 标准库中的完整定义只有一行,是一个映射类型:

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

keyof any 就是 string | number | symbol,即所有可能的键类型。[P in K]: T 为 K 的每个成员创建一个类型为 T 的属性。这就解释了下面的两种行为:像 string 这样宽泛的 K 会生成索引签名(任意键),而联合类型的 K 会为每个成员生成一个必需属性。

联合类型作键:每个键都必须存在

当键是字面量的联合类型时,Record 必须列出其中的每一个键,而且不能有别的键。这样编译器就变成了一张检查清单:

给 Status 加上 "cancelled",这两个对象就会编译失败,直到你为新状态补上标签和颜色。漏掉一个键,或者加上一个不在联合类型中的键,都是编译错误:

index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.

用字符串枚举作键类型也是一样:Record<Color, string> 要求枚举的每个成员都有一项。

Record<string, T> 与缺失的键

用 string 作键时,任何键都是允许的,而且 TypeScript 会把每次读取的类型都当作 T,哪怕这个键根本不存在。在运行时,缺失的键会得到 undefined:

这是 Record 最常见的 bug。有三种处理方式:读取之前先用 in 或 Object.hasOwn 检查;把值声明为 V | undefined;或者开启编译选项 noUncheckedIndexedAccess,它会给项目中每一次索引签名读取都加上 | undefined。用联合类型作键的 Record 没有这个问题,因为每个键都保证存在。

Partial<Record<K, V>>:只需要部分键

想用联合类型作键、又不要求所有键都存在,就把 Record 包在 Partial 里。这样读取会返回 V | undefined,类型如实反映了情况:

拼错的键(比如 jp)仍然会报错,这正是它比 Record<string, string> 强的地方。

遍历 Record

Object.keys、Object.values 和 Object.entries 都能用。要注意的是键的类型:Object.keys 返回 string[],Object.entries 返回 [string, V][],永远不会是你的键联合类型:

TypeScript 故意把键保留为 string:对象在运行时拥有的属性可能比它的类型列出的更多,所以一般来说承诺返回 Plan[] 是不安全的。对于像 seats 这样由字面量创建的对象,这个断言是安全的。

用数据构建 Record

Record 通常是对数组做分组或建立索引的结果。从一个带 Record 类型的空对象开始,再往里填数据:

Book["genre"] 复用了接口中的联合类型作为键类型,所以给 Book 新增一个类别后,byGenre 就会要求补上对应的一项。

Record<string, unknown> 与接口

Record<string, unknown> 常用来表示“某个键为字符串的对象”。它接受对象字面量和用 type 别名声明类型的值,但会拒绝接口:

index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
  Index signature for type 'string' is missing in type 'User'.

接口可以通过声明合并来扩展,所以 TypeScript 不会假定它符合索引签名;类型别名无法重新打开,所以 type User = { name: string } 能通过检查。常见的修复方法有:改为接受 object(照样可以对它调用 Object.keys)、把函数写成泛型(<T extends object>(obj: T)),或者用 type 声明 User(interface 与 type 一文介绍了两者的其他区别)。

Record、索引签名与 Map 对比

Record<K, V>{ [key: string]: V }Map<K, V>
运行时是否存在否,是普通对象否,是普通对象是,是一个类
固定的键集合是,K 为联合类型时否否
键的类型string、number、symbol、字面量联合类型、模板模式string、number、symbol、模板模式任意类型,包括对象
读取缺失键的类型V(string 作键时)Vget 返回 V | undefined
与具名属性混用通过交叉类型 &可以,写在同一个类型里不可以
JSON 与展开运算符支持支持不支持,需先转换
大小Object.keys(r).lengthObject.keys(o).lengthm.size
频繁增删可以可以专为此设计

只要键的集合是已知的,就选用联合类型作键的 Record:它是唯一能检查每个键都存在的方案。对于不固定的字符串键,Record<string, V> 和索引签名可以互换,很多代码库为了可读性更喜欢用 Record。当键会在运行时增删、键不是字符串,或者你需要直接拿到大小和插入顺序时,就用 Map。

常见问题

TypeScript 中的 Record 是什么?

Record<K, V> 是一个内置工具类型,表示键的类型为 K、所有值的类型都为 V 的对象。Record<string, number> 是键为任意字符串、值为数字的对象;Record<"en" | "de", string> 是恰好拥有 en 和 de 两个键、值都为字符串的对象。

TypeScript 中 Record 和 Map 有什么区别?

Record 是普通 JavaScript 对象的类型,所以它能配合对象字面量、JSON 和展开运算符使用,并且在编译时消失。Map 是运行时的类,提供 get、set、has 和 size,对所有键都保留插入顺序,接受任意类型的键(包括对象),而且 get 返回 V | undefined。固定的或 JSON 形状的数据用 Record,运行时会增删键的场景用 Map。

Record<string, T> 和 { [key: string]: T } 有什么区别?

就值而言它们是同一个类型:Record<string, T> 展开后就是带字符串索引签名的对象类型。有两个小区别:索引签名可以带一个名字,并且能和其他属性写在同一个类型里;keyof Record<string, T> 是 string,而 keyof { [key: string]: T } 是 string | number。

如何在 TypeScript 中遍历 Record?

用 Object.entries(record) 获取键值对,用 Object.keys 获取键,用 Object.values 获取值。返回的键是 string 而不是 K,因为对象在运行时可能带有额外的键。当 Record 的键是已知的联合类型时,可以做类型断言:(Object.keys(r) as Array<keyof typeof r>)。

如何让 Record 只有部分键是必需的?

使用联合类型作键时,Record<K, V> 要求每个键都存在。用 Partial 包一层就能让所有键都变成可选:Partial<Record<Lang, string>>。如果要混合,就用交叉类型:Record<"en", string> & Partial<Record<"de" | "fr", string>> 要求必须有 en,其余的键可有可无。

Coddy 编程语言插图

和 Coddy 一起学编程

开始