Menu

TypeScript keyof:把对象的键变成类型(附示例)

keyof 接收一个对象类型,得到由它的属性名组成的联合类型。本文讲解 keyof 与接口、用于普通对象的 keyof typeof、结合泛型的类型安全属性访问、索引签名(string | number),以及 Object.keys 为什么返回 string[]。

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

keyof 接收一个对象类型,得到由它的属性名组成的联合类型。下面的 keyof User 就是类型 "id" | "name" | "email",所以这个类型的变量只能存放这三个字符串之一。

keyof 只存在于类型系统中。代码编译时它会被擦除,所以它从不在运行时检查任何东西。它提供的是编译时的保证:一个键一定对应真实存在的属性。在接口中把 email 改个名字,每一个类型为 keyof User 的 "email" 都会变成错误。

keyof typeof:普通对象的键

keyof 作用于类型,而不是值。对于你以值的形式写出的对象,先用 typeof 获取它的类型,再取它的键:

从里往外读:typeof colors 是 { red: string; green: string; blue: string },再对它取 keyof 就得到键的联合类型。不加 typeof 直接写 keyof colors 会报错:error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?。

这是从查找表推导出联合类型的常用方法:往对象里加一种颜色,Color 类型也会随之扩展。typeof 还有第二种含义,即运行时的含义;typeof 页面对两者都有介绍。

结合泛型的类型安全属性访问

keyof 最常见的用途是作为泛型约束。K extends keyof T 表示“K 是 T 的某个键”,而 T[K] 是这个键对应的属性类型:

第二个类型参数很重要。如果只写 key: keyof T 而没有 K,返回类型就是 T[keyof T],也就是所有属性类型的联合(这里是 string | number | boolean),于是 title.toUpperCase() 会报错:error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean'。用 K 捕获确切的键,就能保留确切的属性类型。T[K] 是索引访问类型,在索引访问类型页面中有介绍。

用普通 string 作索引

当你用来自别处的 string 去索引一个对象时,会出现一个非常常见的错误:

编译器输出:

index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
  No index signature with a parameter of type 'string' was found on type 'Settings'.

任何字符串都可能传到 read,包括 "colour",而 Settings 对这个键一无所知。有两种修复方法:只接受真实存在的键,或者在使用未知字符串之前先检查它。

name is keyof Settings 是一个类型谓词:当函数返回 true 时,TypeScript 在 if 内部把 input 当作一个键。类型守卫页面 详细讲解了类型谓词。

keyof 与索引签名:string | number

对于带索引签名的类型,keyof 返回索引的类型,而字符串索引签名得到的是 string | number,不只是 string:

JavaScript 会把每个数字属性键都转换成字符串,所以 scores[42] 和 scores["42"] 是同一个属性。TypeScript 通过包含 number 来模拟这一点。输出还展示了一条 JavaScript 规则:类似整数的键会按升序排在最前面,然后才是其他字符串键。当泛型函数只需要字符串键时,写 Extract<keyof T, string> 或 string & keyof T;Exclude 和 Extract 有专门的页面。

keyof 结果一览

类型keyof 得到
{ id: number; name: string }"id" | "name"
{ [key: string]: number }string | number
{ [index: number]: string }number
{}never
anystring | number | symbol
{ a: 1; b: 2 } | { a: 3; c: 4 }"a"(每个成员都有的键)
{ a: 1 } & { c: 4 }"a" | "c"(任意一边的键)
string[]number 加上所有数组方法名("length"、"push" 等)

联合类型和交叉类型这两行乍一看是反的。联合类型的值可能是其中任意一个成员,所以只有两边都有的键才是安全的。交叉类型的值拥有两边的全部内容,所以它拥有所有的键。

Object.keys 为什么返回 string[]

Object.keys(obj) 的类型是 string[],永远不是 (keyof T)[]。原因在于结构类型:一个值携带的属性可以比它声明的类型更多。

p 的类型是 Point,但它在运行时有一个 z 键。如果 Object.keys 承诺返回 ("x" | "y")[],依赖这一点的代码对 p 来说就是错的。对于你自己创建的对象字面量,这种类型转换是安全的;对于来自外部的对象,把键保留为 string 并加以检查。

映射类型中的 keyof

keyof 也是遍历一个类型的键、构建新类型的方式。内置的 Partial 和 Readonly 工具类型就是这样写的:

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

interface User {
  id: number;
  name: string;
}

type UserDraft = MyPartial<User>; // { id?: number; name?: string }

[K in keyof T] 逐个访问每个键,T[K] 读取这个键对应的属性类型。映射类型有专门的页面,介绍修饰符和键重映射。

常见问题

TypeScript 中 keyof 有什么作用?

keyof T 得到由类型 T 的属性名组成的联合类型。对于 interface User { id: number; name: string },keyof User 是 "id" | "name"。它只作用于类型,只在编译时起作用,不会生成任何 JavaScript。

TypeScript 中的 keyof typeof 是什么?

keyof 需要一个类型,而 const colors = { red: "#f00" } 这样的普通对象是一个值。typeof colors 把这个值转成它的类型,然后 keyof typeof colors 得到它的键组成的联合类型,这里是 "red"。直接写 keyof colors 会报错 TS2749。

为什么 keyof 返回 string | number?

当类型带有 { [key: string]: number } 这样的字符串索引签名时就会这样。JavaScript 会把数字属性键转换成字符串,所以 obj[42] 也是合法的访问,TypeScript 因此在键类型中包含了 number。如果只想要字符串键,使用 Extract<keyof T, string> 或 string & keyof T。

为什么 Object.keys 返回 string[] 而不是 (keyof T)[]?

因为一个对象拥有的属性可能比它的类型列出的更多:带有额外字段的值可以赋给更窄的类型。对于这些对象,把结果标注为 (keyof T)[] 就是在说谎。如果你确定对象恰好只有声明的那些键,就做类型转换:Object.keys(obj) as (keyof typeof obj)[]。

如何修复“Element implicitly has an 'any' type because expression of type 'string' can't be used to index type”?

索引是普通的 string,而对象类型没有字符串索引签名。把参数类型写成 keyof YourType 而不是 string,或者先用类型守卫检查这个字符串,把它收窄为 keyof YourType;如果对象确实接受任意键,就给它一个索引签名或 Record<string, V> 类型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始