在上一篇文章中,我们学习了如何使用 extends 关键字给泛型加上约束(比如 <T Lengthwise extends>),强制要求传入的参数必须具备某些特定的结构。
但在真实的底层框架开发中,我们会遇到这样一种非常严苛的场景:我们不仅要限制传入的对象,还要限制传入的 “属性名(key)”,并且要求这个属性名必须是该对象身上真实存在的。
为了解决这个问题,TypeScript 为我们提供了一个专门用来 “提取” 对象键名的修饰符:keyof。
TypeScript 中的 keyof 是什么?
keyof 的字面意思是 “xxx 的键”。在 TypeScript 中,keyof 的作用非常简单:接收一个对象类型,并提取出该对象所有公开的键名,并组合成一个联合类型。
注意: keyof 后面跟的必须是一个 “类型”,而不能是一个具体的 “值”。
示例 1:keyof 的基础用法
// 定义接口
interface User {
id: number;
username: string;
avatar: string;
}
// 使用 keyof 提取 User 的所有键名
type UserKeys = keyof User;
const key: UserKeys = "username"
console.log(key);运行结果如下。
username分析:
执行了 type UserKeys = keyof User; 之后,此时 UserKeys 就等价于以下联合类型:
"id" | "username" | "avatar"对于这个例子来说,如果我们使用下面代码,则 TypeScript 会直接报错:
const key: UserKeys = "password"
// 报错:不能将类型 "password" 分配给类型 “keyof User”TypeScript keyof 结合 extends
光看 keyof 的基础用法,小伙伴们可能会觉得它还只是个玩具。但当我们把它和泛型结合起来时,真正的威力才开始显现出来。
假设我们要封装一个全局通用的 getProperty() 函数:传入一个对象和一个属性名,返回该对象对应的值。如果不使用 keyof,大多数初学者会写出下面这种 “到处漏风” 的代码。
示例 2:危险的属性读取
// 试图使用 object 和 string 糊弄过去
function getProperty(obj: object, key: string) {
// 这里的读取非常危险,TypeScript 会直接报错
return obj[key];
// 报错:元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "{}"
}分析:
上面这段代码犯了两个致命错误:
- key 是普通的 string,意味着外部可以瞎传入诸如 "admin_password" 这种根本不存在的属性名。
- 返回值失去了类型推导,TypeScript 根本不知道你取出来的到底是个什么玩意,只能悲剧地退化为隐式的 any。
为了打造一个绝对安全、且带完美类型推导的 getProperty() 函数,我们需要借助泛型约束和 keyof 来制定极其严苛的规则:
- 规则 1:对象是泛型 T。
- 规则 2:属性名是泛型 K,且 K 必须是 T 身上存在的键(K extends keyof T)。
- 规则 3:返回值必须是对象 T 中键 K 对应的值类型(T[K])。
示例 3:完美的 getProperty() 函数
// 定义函数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const myUser = {
id: 1001,
name: "Jack",
isAdmin: true
};
// 调用 1:获取 name
// TS 自动推断出返回值 strRes 必然是 string 类型
const strRes = getProperty(myUser, "name");
console.log(strRes.length);
// 调用 2:获取 isAdmin
// TS 自动推断出返回值 boolRes 必然是 boolean 类型
const boolRes = getProperty(myUser, "isAdmin");
console.log(boolRes);运行结果如下。
4
true分析:
上面这个例子实现的,其实是 Vue 和 React 源码中最常见的 “泛型体操”。当调用 getProperty(myUser, "name") 时,TypeScript 在底层进行了精密的演算:
- 推断出 T 是:{ id: number; name: string; isAdmin: boolean }。
- 推断出 keyof T 是:"id" | "name" | "isAdmin"。
- 检查你传入的 K(即 "name")是否在上面的集合中。完美命中!
- 计算返回值 T[K](即 T["name"]),精准推断出返回值类型是 string。
TypeScript 进阶技巧:keyof typeof
前面我们再三强调,keyof 后面必须跟一个 “类型”。但在实际写业务逻辑时,我们手里往往只有一个现成的 JavaScript 对象(值),并没有专门为它写过 interface。这时候,应该如何提取这个 “值” 的键名集合呢?
此时就需要使用到 TypeScript 中的 typeof 关键字了。在 TypeScript 的类型世界中,我们可以使用 typeof 关键字来将一个具体的 “值” 反向推导为 “类型”。
示例 4:无 interface 下的键名提取
// 定义一个普通对象(没有为它定义过 interface)
const appConfig = {
theme: "dark",
language: "zh-CN",
timeout: 5000
};
// 先用 typeof 把值转成类型,再用 keyof 提取键名
type ConfigKeys = keyof typeof appConfig;
const validKey: ConfigKeys = "theme";
console.log(validKey);运行结果如下。
theme分析:
keyof typeof appConfig 表示先使用typeof 把值转成类型,再用 keyof 提取出键名。此时 ConfigKeys 等价于:
"theme" | "language" | "timeout"需要注意,下面写法是错误的。这是因为 keyof 后面不能直接跟变量(值)。
type ConfigKeys = keyof appConfig;
// 报错:“appConfig” 表示值,但在此处用作类型。是否指 “类型 appConfig” ?TypeScript 进阶技巧:T[keyof T]
在前面的示例 3 中,我们知道了 T[K] 可以获取某一个特定键的值类型。那如果把 K 替换成所有的键(也就是 keyof T),会发生什么奇妙的反应呢?
答案是:我们可以直接提取出一个对象所有 “值” 的联合类型!这在处理常量字典、状态枚举时非常好用。
示例 5:提取配置对象的所有值类型
// 使用 as const 断言,冻结对象的字面量类型
const statusMap = {
SUCCESS: 200,
NOT_FOUND: 404,
ERROR: 500,
LOADING: "loading"
} as const;
// 第 1 步:用 typeof 获取对象的整体类型
type StatusType = typeof statusMap;
// 第 2 步:用 keyof 获取所有键名 ("SUCCESS" | "NOT_FOUND" | "ERROR" | "LOADING")
type StatusKeys = keyof StatusType;
// 第 3 步:提取所有值的联合类型
type StatusValues = StatusType[StatusKeys];
// 测试:变量只能被赋值为 statusMap 中真实存在的值的类型
const val1: StatusValues = 200;
const val2: StatusValues = "loading";
// 错误调用:401 不在允许的值范围内,被成功拦截!
// const val3: StatusValues = 401;
// 报错:不能将类型 “401” 分配给类型 “200 | 404 | 500 | "loading"分析:
在企业级开发中,我们通常会把上面这 3 步简写为非常经典的一行代码:
type StatusValues = typeof statusMap[keyof typeof statusMap];这行代码看起来像天书,但只要小伙伴们掌握了本文前面讲的知识点,把它拆解开来就很好理解了:无非就是先拿到类型的 Key,然后再通过 Type[Key] 去获取值。
keyof any 是什么?
在阅读一些顶级开源源码时,我们偶尔会看到 type K = keyof any; 这种写法。
这是一个冷门但有用的知识点:在 JavaScript 的底层设计中,一个对象的键(key)只能是 3 种类型:数字、字符串或 Symbol。因此,在 TypeScript 中,keyof any 会被直接推导为 string | number | symbol 的联合类型。
因此,keyof any 经常被用来约束一个泛型 “必须能够作为对象的键”。
