TypeScript keyof 关键字

在上一篇文章中,我们学习了如何使用 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 经常被用来约束一个泛型 “必须能够作为对象的键”。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号