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 |
any | string | 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> 类型。