在 TypeScript 中遍历对象键可能会是一场噩梦。以下是我所知的所有解决方案。
简单概念
使用 Object.keys 进行迭代不起作用,因为Object.keys
返回的是一个字符串数组,而不是所有键的联合。这是设计上的,不会改变。
function printUser(user: User) {
Object.keys(user).forEach((key) => {
// 不起作用!
console.log(user[key]);
// 出错信息如下:
// Expression implicitly has an 'any' type because expression of type 'string' can't be used to index type 'User'.
// No index signature with a parameter of type 'string' was found on type 'User'.
});
}
在正确的位置转换为keyof typeof
可以使其正常工作:
const user = {
name: "Daniel",
age: 26,
};
const keys = Object.keys(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;
}
keys.forEach((key) => {
if (isKey(user, key)) {
console.log(user[key]);
}
});
更详细的解释
Object.keys
问题在于:使用 Object.keys
似乎不会按照你的预期工作。这是因为它没有返回你需要的类型。
它并不是一个包含所有键的类型,而是将其扩展为字符串数组。
const user = {
name: "Daniel",
age: 26,
};
const keys = Object.keys(user);
const keys: string[]
这意味着你无法使用键来访问对象上的值:
const nameKey = keys[0];
const nameKey: string
user[nameKey];
// 出错信息如下:
// Expression implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; age: number; }'.
// No index signature with a parameter of type 'string' was found on type '{ name: string; age: number; }'.
TypeScript
在这里返回字符串数组是有充分理由的。TypeScript
对象类型是开放式的。
有许多情况下,TS
无法保证 Object.keys
返回的键实际上在对象上 - 所以将它们扩展为字符串是唯一合理的解决方案。详细信息请参阅此问题。
for...in 循环
如果你尝试使用for...in
循环,你也会发现它会失败。这是因为相同的原因 - key
被推断为字符串,就像 Object.keys
一样。
function printUser(user: User) {
for (const key in user) {
console.log(user[key]);
// 出错信息如下:
// Expression implicitly has an 'any' type because expression of type 'string' can't be used to index type 'User'.
// No index signature with a parameter of type 'string' was found on type 'User'.
}
}
但对于许多情况,你可能会有信心确切地知道对象的形状。
那么,你该怎么做呢?
解决方案 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
仍然被类型化为字符串 - 但在我们索引 user
时,我们将其转换为 keyof typeof user
。
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];
在这种情况下,as
是一个相当强大的工具 - 如你所见,它让我们对 TypeScript
关于某些事物的类型进行欺骗。
解决方案 2: 类型判断
让我们看一些更智能、可能更安全的解决方案。使用类型谓词怎么样?
通过使用 isKey
辅助函数,我们可以在索引对象之前检查键是否确实在对象上。
通过在 isKey
的返回类型中使用 is
语法,我们让 TypeScript
正确地推断。
function 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]);
}
});
这个很棒的解决方案来自于 Stefan Baumgartner 在这个主题上的博文。
解决方案 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);
这可能是最容易被误用的解决方案 - 将转换隐藏在函数内部使其更具吸引力,可能会导致人们在不加思考的情况下使用它。
结论
我的首选解决方案?通常情况下,转换完全可以胜任。它简单易懂 - 通常足够安全。
但如果你喜欢类型谓词或泛型解决方案,那就去试试吧。isKey
函数看起来足够有用。
网友评论