索引访问类型用来查找另一个类型上某个属性的类型,语法和 JavaScript 访问属性时的方括号一样。Person["age"] 是 number,Person["address"]["city"] 是 string。
好处是只有唯一的数据来源。Age 并不是 number 的第二份副本:如果 Person["age"] 改成了 string,所有写成 Person["age"] 的类型都会随之改变。索引访问类型只在编译时存在,不会生成任何 JavaScript 代码。
联合类型的键与 T[keyof T]
索引可以是由多个键组成的联合类型,结果就是这些属性类型组成的联合类型:
Product[keyof Product] 是在类型层面表达“Product 的任意一个属性值”的方式。访问不存在的键是一个编译错误:Product["price"] 会得到 error TS2339: Property 'price' does not exist on type 'Product'。
可选属性包含 undefined
开启 strictNullChecks 时(strict 下默认开启),可选属性的类型包含 undefined,它的索引访问类型也一样:
type Profile = { name: string; nickname?: string };
type Nick = Profile["nickname"]; // string | undefined
要去掉 undefined,就把它包起来:NonNullable<Profile["nickname"]> 是 string。
用 T[number] 获取数组元素类型
数组是用数字索引的,所以用 number 索引一个数组类型就能得到元素类型。它还能穿过好几层:
当一个类型来自你无法控制的地方时,比如自动生成的 API 客户端,这种写法就很有用:ApiResponse["data"]["users"][number] 不需要任何人导出,就能拿到用户类型。
元组:按位置索引
元组类型可以用具体的位置来索引,用 number 得到所有元素类型的联合,用 "length" 得到它的长度(一个字面量类型):
(typeof arr)[number]:从 const 数组得到联合类型
实际开发中最常见的用法:把允许的值列表保存为一个数组(这样运行时可以遍历它),再从中派生出联合类型(这样编译器可以检查它)。
这里有两个关键点。as const 把数组变成由字面量类型组成的只读元组;没有它,ROLES 就是 string[],(typeof ROLES)[number] 也只是 string。然后 typeof 把值变成类型,[number] 把它的元素收集成一个联合类型。括号是可选的(typeof ROLES[number] 的意思相同),但加上括号能让运算顺序一目了然。
检查字符串是否在数组中
这个只读元组有一个让人意外的地方:它的 includes 方法只接受元素类型,所以你不能把任意字符串传给它:
index.ts(6,25): error TS2345: Argument of type 'string' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
检查时把数组放宽为 readonly string[],并把函数改成一个类型守卫,这样检查成功后值就会被收窄:
这种模式让一个列表同时承担两项工作:运行时检查使用这个数组,类型也从它派生而来,所以两者永远不会脱节。
泛型中的索引访问:T[K]
在泛型中,配合 K extends keyof T 使用的 T[K],就是调用者选中的那个属性的类型:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const users = [
{ name: "Ada", age: 36 },
{ name: "Linus", age: 28 },
];
const names = pluck(users, "name"); // string[]
const ages = pluck(users, "age"); // number[]
正是 K extends keyof T 这个约束让 T[K] 合法。没有它,TypeScript 无法知道 K 是 T 的键,会报告 error TS2536: Type 'K' cannot be used to index type 'T'。
语法一览
| 写法 | 含义 | 示例结果 |
|---|---|---|
T["key"] | 某一个属性的类型 | Person["age"] 是 number |
T["a" | "b"] | 这些属性类型的联合 | string | number |
T[keyof T] | 所有属性类型的联合 | 每一种值的类型 |
T["a"]["b"] | 嵌套属性 | Person["address"]["city"] |
Arr[number] | 数组元素类型 | User[] 得到 User |
Tup[0] | 元组某个位置的元素 | 第一个元素的类型 |
Tup["length"] | 元组长度(字面量) | 3 |
(typeof arr)[number] | const 数组中各个值的联合 | "admin" | "editor" |
T[typeof key] | 用常量的类型作为索引 | 等同于 T["name"] |
索引永远是类型。当 key 是一个 const 变量时,Person[key] 会因两个错误而失败:error TS2538: Type 'key' cannot be used as an index type. 和 error TS2749: 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?。
常见问题
TypeScript 中的索引访问类型是什么?
它是写作 T[K] 的类型,用来查找类型 T 上属性 K 的类型。对于 type Person = { name: string; age: number },Person["age"] 是 number。它使用和 JavaScript 属性访问相同的方括号语法,但是在编译时作用于类型。
TypeScript 中如何获取数组元素的类型?
用 number 索引数组类型:对于 type Users = User[],Users[number] 就是 User。如果是数组值,就结合 typeof 使用:(typeof users)[number]。
如何把字符串数组变成联合类型?
用 as const 声明数组,让 TypeScript 保留字面量值,然后用 number 索引它:const roles = ["admin", "editor"] as const; type Role = (typeof roles)[number]; 得到 "admin" | "editor"。不加 as const 的话,元素类型只是 string。
为什么不能在索引访问类型中用变量作为索引?
索引必须是类型。在 const key = "name" 的情况下写 Person[key] 会失败(错误 TS2538 和 TS2749),因为 key 是一个值。应该写 Person[typeof key],或者直接使用字面量:Person["name"]。
T[keyof T] 是什么意思?
它用 T 自身所有键组成的联合类型去索引它,所以结果是所有属性类型组成的联合类型。对于 { a: string; b: number },结果是 string | number。这是在类型层面表达“这个对象的任意一个值”的方式。