使用TypeScript时迭代对象的最简单方法



挑战:安全而简单地迭代对象,并使用TypeScript的括号表示法访问属性。

到目前为止,我找到的最好的解决方案如下,但我想进一步简化:

for (const key of propList) {
if (!(key in propList)) continue; // Required to avoid: for (... in ...) statements must be filtered with an if statement
const safeKey = key as keyof typeof myObj; 
console.log(myObj[safeKey]);
}

我正在寻找一些更简单的东西(这是无效的代码,只是试图表达我的目标,以消除多余的常量):

for (const (safeKey keyof typeof myObj) of propList) {
console.log(myObj[safeKey]);
}

我考虑过这些,但不认为它们是可取的:

// Undesirable option #1 (potentially requires repetitive type assertions)
for (const key in myObj) {
console.log(myObj[key as keyof typeof myObj]); //works, but have to repeat the type assertion for every use
}
//Undesirable option #2 (creates scope issues)
let key: keyof typeof myObj;
for (key in myObj) {
console.log(myObj[key]); //works, but may create scope issues requiring a closure (such as if using this pattern to create a bunch of buttons with click handlers)
}

我考虑过这些,但它们都是无效或不完整的解决方案:

//Invalid option A
const myObj = { prop1: true, prop2: true };
for (const key in myObj) {
console.log(myObj[key]); //TS error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ prop1: boolean; prop2: boolean; }.
}
// Invalid option B
const myObj = { prop1: true, prop2: true };
for (const key in myObj) {
if (key in myObj) console.log(myObj[key]); //TS still treats key as a string
}
// Invalid option C
for (const key in myObj) {
if (myObj.hasOwnProperty(key)) console.log(myObj[key]); //TS still treats key as a string
}
// Invalid option D
const propList = Object.getOwnPropertyNames(myObj);
for (const key of propList) {
console.log(myObj[key]); //TS treats key as a string because .getOwnPropertyNames() returns string[]
}
// Invalid option E
Object.keys(myObj).forEach((key) => {
console.log(myObj[key]); //TS treats key as a string because .keys() returns string[]
});
//Invalid option F (incomplete: can get, but not set values)
for (const [key, val] of Object.entries(myObj)) {
myObj[key] = false; //TS treats key as a string
}

谢谢你的帮助!

TL;DR:对象类型不准确(尽管有多余的属性检查),因此迭代属性将失去类型保证,除非提供此类保证,这意味着如果不满足保证,将承担责任。。。所以要小心!

您需要使用类型断言(或道德等价物)来告诉编译器,对象只有它所知道的键,并且有一些警告使这种假设可能是错误的(但可能不是可能)并导致运行时错误。


对象类型不准确

TypeScript中的对象类型是打开可扩展,而不是关闭精确(有关支持精确类型的功能请求,请参阅microsoft/TypeScript#12936)。编译器不认为它所知道的属性是对象上可能存在的仅属性。例如,类型{ prop1: boolean; prop2: boolean; }表示";在CCD_ 3和CCD_;,但不是的意思是";除了CCD_ 5和CCD_;。

这对JavaScript非常有用,因为它允许我们将class层次结构视为类型层次结构。如果class Bar extends Foo { extraProp = 123 },那么我知道Bar的任何实例也是Foo的实例。但是,如果TypeScript中的对象类型是精确的,那么Bar中额外的extraProp属性将阻止您将Bar类型的值分配给Foo类型的变量,从而破坏继承。TypeScript的整个结构类型系统是围绕着将对象类型视为打开而构建的。参见手册中interface:的示例

请注意,我们的对象实际上有更多的属性,但编译器只检查是否至少存在所需的属性并匹配所需的类型。


(尽管有多余的属性检查)

不幸的是,在许多情况下,人们期望对象类型被视为封闭的。最明显的是当您处理对象文字时,该文字在处理之前未被修改。在类型检查器期望某个特定对象类型的地方直接使用对象文字的情况下,您将在"上收到警告;额外的";所有物这被称为超额属性检查:

interface MyObj {
prop1: boolean;
prop2: boolean;
}
const myObj: MyObj = { prop1: true, prop2: false, prop3: "oops" }; // error!
// ---------------------------------------------> ~~~~~~~~~~~~~
// Object literal may only specify known properties, 
// but 'prop3' does not exist in type 'MyObj'

但这种对确切类型的认可并没有走多远。您所要做的就是间接使用对象文字;例如,通过首先将其分配给一个类型为推断的变量:

const someObj = { prop1: true, prop2: false, prop3: "oops" };
const myObj: MyObj = someObj; // no error

因此,除了一些例外,TypeScript中的对象类型不会被视为精确的。


因此迭代属性失去类型保证

这导致我们遍历对象的属性。由于对象类型并不精确,TypeScript认为,您迭代其属性的任何对象都可能隐藏着意外的惊喜。这是预期和预期的行为,尽管这通常令人沮丧。请参见为什么Object.keys不返回TypeScript中的类型键?了解更多信息。任何特定对象的键都将被视为stringstring | number | symbol或一些非常宽的东西。试着说"prop1" | "prop2" | string没有帮助,因为那只是string

如果密钥的类型是boolean0,那么当您试图用它实际索引到对象时,编译器会警告您,除非该对象恰好具有string索引签名。警告基本上是告诉你,它不能保证你会看到什么属性类型(如果有的话)。如果您正在查看某个未知的额外属性,则属性类型可以是任意类型,因此它被隐式地视为any。如果使用--noImplicitAny--strict编译器选项,则此类隐式anys(anies?)被视为错误。


除非提供此类担保

因此,您将需要某种变通方法来告诉编译器";我不在乎你是否认为对象不准确,obj的键是keyof typeof obj&";。你已经看过上面的一些,但它们对你来说太多余了。另一种解决方法是制作一个通用的帮助器函数,该函数的实现只调用Object.keys(),但其返回类型您断言prop10:

function keys<T extends object>(obj: T) {
return Object.keys(obj) as Array<keyof T>;
}

有了这个keys()函数,您的迭代可以在没有编译错误的情况下编写:

for (const key of keys(myObj)) {
console.log(myObj[key]); // works now
}

意味着如果不符合担保,您将承担责任

所以,只要你确信你的物体没有任何隐藏在灌木丛中的钥匙准备向你扑来,这可能对你有用。对于你自己创建并立即迭代而不修改的对象文字,这是一个非常安全的选择。对于其他任何事情,在运行时可能会有严重的错误等待你。

如果你不小心将一个属性视为boolean,而它不是CCD_32(除了像=== true=== false这样的检查,大多数值都可以毫无问题地被视为truthy/falsy),那么很难想出太可怕的东西。所以让我们稍微改变一下对象类型:

interface YourObj {
x: string;
y: string;
}
function processYourObj(yourObj: YourObj) {
for (const key of keys(yourObj)) {
console.log(yourObj[key].toUpperCase())
}
}
processYourObj({ x: "hello", y: "there" }); // HELLO THERE

这很好用。但请记住,对象类型并不准确:

const theirObj = { x: "assumption", y: "is", z: false }
processYourObj(theirObj); // no compiler error, but at runtime:
// ASSUMPTION IS 💥 yourObj[key].toUpperCase is not a function

对象theirObj可分配给YourObj,因为对象类型不精确。因此processYourObj()在没有编译器错误的情况下接受theirObj。在运行时,它试图访问prop20的toUpperCase()方法,您的程序就会崩溃。


所以要小心

游乐场链接到代码

最新更新