TypeScript进阶:解锁对象键迭代的隐藏技巧

Authors
  • avatar
    Name
    小明&小艺
    Twitter

在 TypeScript 中迭代对象键可能会出现一些令人抓狂的情况。来看一个例子,应该大多数人都会经历过:

type User = {
  name: string;
  age: number;
};

function printUser(user: User) {
  Object.keys(user).forEach((key) => {
    // Doesn't work!
    console.log(user[key]);
  });
}

除非你知道一些技巧,否则这并不简单。这里是我所知道的所有解决方案。

原因分析

使用 Object.keys 进行迭代不起作用,因为 Object.keys 返回的是一个字符串数组,而不是所有键的联合。这是 TypeScript 有意为之,不能修改的。

常见的解决方法是通过 keyof typeof,将 key 的类型强制转换为对象的 key 值枚举:

const user = {
  name: 'Daniel',
  age: 26,
}

const keys = Object.keys(user) as Array<keyof typeof user>
keys.forEach((key) => {
  console.log(user[key as keyof typeof user])
})

又或者通过内联缩小类型来解决。

function isKey<T extends object>(x: T, k: PropertyKey): k is keyof T {
  return k in x
}

const keys = Object.keys(user)
keys.forEach((key) => {
  if (isKey(user, key as keyof typeof user)) {
    console.log(user[key])
  }
})

这里的问题在于:使用 Object.keys 没有按你预想的那样返回你需要的类型。

Typescript 没有返回包含所有键的类型,而是将其推测为字符串数组。

TypeScript 对于对象类型的设计是开放式的。不可否认,在许多情况下,我们不能保证 Object.keys 返回的键实际上存在于对象上,所以将它们推测为字符串数组是唯一个退一步海阔天空的解决方案。这里有这个问题相关的讨论:github issue

for...in 循环,也同样有类似的问题。

解决方案

也许在许多情况下,你是知道对象的 key 值有哪些的。

那么,你该怎么做呢?

解决方案 1:将类型转换为 keyof typeof

第一个方案是使用 keyof typeof 将键转换为更具体的类型。

在下面的示例中,我们将 Object.keys 的结果转换为包含这些键的数组。

const user = {
  name: 'Daniel',
  age: 26,
}

const keys = Object.keys(user) as Array<keyof typeof user>
keys.forEach((key) => {
  console.log(user[key])
})

也可以将 key 强制转换为对应的类型。

const keys = Object.keys(user)
keys.forEach((key) => {
  console.log(user[key as keyof typeof user])
})

使用 as 在任何形式上通常是不安全的,确实如此,它会对现在或者将来存在一些风险,因为它是人为的指认。

const user = {
  name: 'Daniel',
  age: 26,
}

const nonExistentKey = 'id' as keyof typeof user
const value = user[nonExistentKey]
// 没有错误!

解决方案 2:类型谓词

让我们看看一些更智能、更安全的解决方案。如何使用类型谓词?

通过使用 isKey,我们可以在索引之前检查键是否真的在对象上。

我们通过在 isKey 的返回类型中使用 is 语法,让 TypeScript 正确推断。

function isKey<T extends object>(x: T, k: PropertyKey): k is keyof T {
  return k in x
}

const keys = Object.keys(user)
keys.forEach((key) => {
  if (isKey(user, key)) {
    console.log(user[key])
  }
})

解决方案 3:泛型函数

让我们看看一个稍微奇怪的解决方案。在泛型函数内,使用 in 操作符确实会将类型缩小到键。

function printEachKey<T extends object>(obj: T) {
  for (const key in obj) {
    console.log(obj[key])
  }
}

printEachKey({
  name: 'Daniel',
  age: 26,
})

解决方案 4:将 Object.keys 包装在函数中

另一个解决方案是将 Object.keys 包装在一个返回转换后类型的函数中。

const objectKeys = <T extends object>(obj: T) => {
  return Object.keys(obj) as Array<keyof T>
}

const keys = objectKeys({
  name: 'Daniel',
  age: 26,
})
console.log(keys)

结论

个人更加推荐方案一,因为它更具可读性,理解上也更直观。