原文:How to Iterate Over Object Keys in TypeScript
链接:https://www.totaltypescript.com/iterate-over-object-keys-in-typescript
作者:Matt Pocock
在 TypeScript 中迭代对象的键可能会比较困难。以下是我知道的所有解决方案。
简要说明
使用 Object.keys 进行迭代是行不通的,因为 Object.keys 返回的是一个字符串数组,而不是所有键的联合类型。这是有意设计的,不会更改。
1 | function printUser(user: User) { |
- 在正确的位置进行 keyof typeof 的类型转换可以使其正常工作:
1
2
3
4
5
6
7
8
9
10
11const user = {
name: "Daniel",
age: 26,
};
const keys = Object.keys(user);
keys.forEach((key) => {
//(parameter) key: string
console.log(user[key as keyof typeof user]);
}); - 使用自定义类型断言也可以通过在内联中缩小类型。
1
2
3
4
5
6
7
8
9
10
11
12
13function isKey<T extends object>(
x: T,
k: PropertyKey
): k is keyof T {
return k in x;
}
keys.forEach((key) => {
if (isKey(user, key)) {
console.log(user[key]);
//(parameter) key: "name" | "age"
}
});
更详细的解释
Object.keys
这里的问题是:使用 Object.keys 似乎不像你期望的那样工作。这是因为它并不返回你需要的那种类型。
与其返回一个包含所有键的类型,它将其扩大为一个字符串数组。
1 | const user = { |
这意味着你不能使用该键来访问对象上的值:
1 | const nameKey = keys[0]; |
TypeScript返回一个字符串数组是有充分理由的。TypeScript 对象类型是开放的。
有许多情况下,TS 无法保证由 Object.keys 返回的键实际上存在于对象中 - 因此将它们扩大为字符串是唯一合理的解决方案。请查看这个问题以获取更多详细信息。
#For…in 循环
如果尝试使用 for…in 循环,你也会发现这种方法失败。原因相同 - 循环中的键被推断为字符串,就像 Object.keys 一样。
1 | function printUser(user: User) { |
但在许多情况下,你可能会确信你完全了解对象的形状。
那么,你该怎么办呢?
解决方案1:转换为 keyof typeof
第一个选项是使用 keyof typeof 将键转换为更具体的类型。
在下面的例子中,我们将 Object.keys 的结果转换为包含这些键的数组。
1 | const user = { |
当我们索引对象时,我们也可以进行类型转换。
在这里,key
仍然被类型推断为字符串 - 但是在我们索引到 user
时,我们将其转换为 keyof typeof user
。
1 | const keys = Object.keys(user); |
在任何形式中使用as
通常是不安全的,而这也不例外。
1 | const user = { |
as
在这种情况下是一个相当强大的工具 - 正如你所见,它允许我们欺骗 TypeScript
关于某物的类型。
解决方案2:类型谓词
让我们看一些更智能、更安全的解决方案。使用类型推断怎么样?
通过使用 isKey
辅助函数,我们可以在索引之前检查键是否确实存在于对象中。
通过在 isKey
的返回类型中使用 is 语法,我们使 TypeScript 正确地进行类型推断。
1 | function isKey<T extends object>( |
这个出色的解决方案来自于 Stefan Baumgartner 在这个主题上的博客文章。
#解决方案3:泛型函数
让我们看一个稍微奇怪的解决方案。在一个泛型函数内部,使用 in 运算符将缩小类型到键。
我实际上不确定为什么这个版本能够工作,而非泛型版本不能。
1 | function printEachKey<T extends object>(obj: T) { |
解决方案4:将 Object.keys 包装在一个函数中
另一种解决方案是将 Object.keys 包装在一个函数中,该函数返回转换后的类型。
1 | const objectKeys = <T extends object>(obj: T) => { |
这可能是最容易被滥用的解决方案 - 将转换隐藏在函数内部使其更具吸引力,并可能导致人们在不加思考的情况下使用它。
结论
我的首选解决方案是仍然是进行类型转换。它简单易懂,并且通常足够安全。
但是,如果你喜欢类型谓词或泛型解决方案,那就尽管使用。isKey 函数看起来足够有用,我可能会在我的下一个项目中借鉴它。