在 TypeScript 中输入具有未知属性的对象。怎样做才正确呢?

Gre*_*222 4 javascript types object typescript

假设有一种类型:

\n
\ninterface ObjWithKnownKeys {\n  prop: string;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我需要一个包含这种类型的对象的对象。我知道我可以这样输入:

\n
interface ObjWithUnknownKeysA {\n  [key: string]: ObjWithKnownKeys;\n}\n\nconst a: ObjWithUnknownKeysA = {};\n\n// \xe2\x9d\x8c no error that 'someName' is possibly undefined\na.someName.prop;\n\nfor (const key in a) {\n  const entry = a[key];\n  // \xe2\x9c\x94\xef\xb8\x8f no errors\n  entry.prop;\n}\nfor (const [key, entry] of Object.entries(a)) {\n  // \xe2\x9c\x94\xef\xb8\x8f no errors\n  entry.prop;\n}\n\n
Run Code Online (Sandbox Code Playgroud)\n

或者像这样:

\n
interface ObjWithUnknownKeysB {\n  [key: string]: ObjWithKnownKeys | undefined;\n}\n\nconst b: ObjWithUnknownKeysB = {};\n\n// \xe2\x9c\x94\xef\xb8\x8f error that 'someName' is possibly undefined\nb.someName.prop;\n\nfor (const key in b) {\n  const entry = b[key];\n  // \xe2\x9d\x8c error that entry is possibly undefined (but it's clear it's not for given key)\n  entry.prop;\n}\nfor (const [key, entry] of Object.entries(b)) {\n  // \xe2\x9d\x8c error that entry is possibly undefined\n  entry.prop;\n}\n\n
Run Code Online (Sandbox Code Playgroud)\n

但是我应该如何输入它以确保它始终按预期工作?

\n

自动关闭后编辑:\n在标记为类似的线程中没有提及 --noUncheckedIndexedAccess 标志。我认为这个标志是一个更好的答案。

\n

jca*_*alz 5

这是 TypeScript 的痛点之一。默认情况下,编译器将具有索引签名的类型视为存在并定义了相关键类型的每个可能属性。这很方便,因为您不必在使用属性之前让编译器相信属性已实际定义:

interface StringIndex {
   [k: string]: string;
}
const str: StringIndex = { abc: "hello" };
str.abc.toUpperCase(); // okay
Run Code Online (Sandbox Code Playgroud)

您可以轻松地迭代键/值:

for (const k in str) {
   str[k].toUpperCase(); // okay
}

for (const v of Object.values(str)) {
   v.toUpperCase(); // okay
}

// arrays have numeric index signatures
const arr: Array<string> = ["x", "y", "z"];
for (const s of arr) {
   s.toUpperCase(); // okay
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,它显然是不安全的:

str.boop.toUpperCase(); // no compiler error!     
Run Code Online (Sandbox Code Playgroud)

因此,microsoft/TypeScript#13778上有一个长期的功能请求,要求编译器查看从索引签名读取可以为您提供undefined. 这得到了开发者社区的大力支持。


长期以来,如果想要安全,只能手动添加| undefined属性值类型。这使事情变得更安全,但现在您必须不断告诉编译器您的属性已定义,并且您还可以写入属性 undefined

interface StringIndex {
   [k: string]: string | undefined; // manually add undefined
}
const str: StringIndex = { abc: "hello" };
str.abc.toUpperCase(); // error! possibly undefined
str.abc = undefined; // no error, but we don't want to allow this
Run Code Online (Sandbox Code Playgroud)

迭代也受到以下污染undefined

for (const k in str) {
   str[k].toUpperCase(); // error! ossibly undefined
}

for (const v of Object.values(str)) {
   v.toUpperCase(); // error! ossibly undefined
}

const arr: Array<string | undefined> = ["x", "y", "z"];
for (const s of arr) {
   s.toUpperCase(); // error! possibly undefined
}
Run Code Online (Sandbox Code Playgroud)

这很烦人。


最终,TypeScript 引入了编译--noUncheckedIndexedAccess器选项,它会自动添加您从索引签名键读取的undefined属性类型,但不允许您对其进行写入。对于迭代,它不会添加in循环或 in / : undefinedundefinedfor...ofObject.values()Object.entries()

// enable --noUncheckedIndexedAccess
interface StringIndex {
   [k: string]: string;
}
const str: StringIndex = { abc: "hello" };
str.boop.toUpperCase(); // error! possibly undefined
str.abc = undefined; // error!

for (const v of Object.values(str)) {
   v.toUpperCase(); // okay
}

const arr: Array<string> = ["x", "y", "z"];
for (const s of arr) {
   s.toUpperCase(); // okay
}
Run Code Online (Sandbox Code Playgroud)

但它并不完美。它仍然无法识别存在“已知”密钥:

const str: StringIndex = { abc: "hello" };
str.abc.toUpperCase(); // error! still possibly undefined
Run Code Online (Sandbox Code Playgroud)

它不允许您for...in在不考虑以下情况的情况下使用循环迭代键undefined

for (const k in str) {
   str[k].toUpperCase(); // error, oops
}
Run Code Online (Sandbox Code Playgroud)

因此,您会得到一组不同的理想/不良行为--noUncheckedIndexedAccess,但您永远不会得到“在所有情况下都正确的事情”。


发生这种情况的原因是编译器不跟踪变量的身份;它跟踪它们的类型。它可以缩小变量的类型,但它没有办法将变量标记为“某某对象的已知当前键”。编译器根本不知道如何“做正确的事情”。一般来说,开启--noUncheckedIndexedAccess会让事情变得更烦人,人们开始绕过它而不是关注它。这就是为什么它不是编译器选项套件--strict一部分。来自microsoft/TypeScript#13778 的评论

想想世界上的两种类型的键:那些你知道在某个对象中相应属性的键(安全),那些你不知道在某个对象中有相应属性的键(危险)。通过编写正确的代码,如 [afor。您可以从密钥中获得第二种类型,即“危险”类型,可以从用户输入或磁盘中的随机 JSON 文件或某些可能存在但可能不存在的密钥列表中获得。

所以如果你有一把危险类型的钥匙和它的索引,最好放在| undefined这里。但该提案不是“将危险钥匙视为危险钥匙”,而是“将所有钥匙,甚至安全钥匙视为危险钥匙”。一旦你开始将安全钥匙视为危险,生活就变得很糟糕。你写的代码就像

for (let i = 0; i < arr.length; i++) { console.log(arr[i].name); }
Run Code Online (Sandbox Code Playgroud)

TypeScript 正在向你抱怨,arr[i]尽管undefined看起来我只是@#%#ing 测试了它。现在你养成了编写这样的代码的习惯[使用非空断言],这感觉很愚蠢:

for (let i = 0; i < arr.length; i++) { console.log(arr[i]!.name); }
Run Code Online (Sandbox Code Playgroud)

或者你可能会写这样的代码:

function doSomething(myObj: T, yourObj: T) {
  for (const k of Object.keys(myObj)) { console.log(yourObj[k].name); }
}
Run Code Online (Sandbox Code Playgroud)

TypeScript 说“嘿,索引表达式可能是| undefined,所以你尽职尽责地“修复它”,因为你已经看到这个错误 800 次了:

function doSomething(myObj: T, yourObj: T) {
  for (const k of Object.keys(myObj)) { console.log(yourObj[k]!.name); }
}
Run Code Online (Sandbox Code Playgroud)

但你并没有修复这个bug。你本来想写Object.keys(yourObj),或者也许myObj[k]。这是最糟糕的编译器错误,因为它实际上在任何情况下都没有帮助您 - 它只是将相同的仪式应用于每种表达式,而不考虑它实际上是否比相同形式的任何其他表达式更危险。

我想起了以前的“你确定要删除这个文件吗?” 对话。如果每次您尝试删除文件时都会出现该对话框,那么您很快就会学会del y在过去点击时点击del,并且不删除重要内容的机会会重置为对话前基线。相反,如果该对话框仅在您删除文件且文件不进入回收站时出现,那么现在您就拥有了有意义的安全性。但我们不知道(也不可能)您的对象键是否安全,因此显示“您确定要索引该对象吗?” 每次执行此操作时都显示对话框,与不显示全部内容相比,不太可能以更高的速度找到错误。

因此,由于编译器无法区分“安全”键和“危险”键之间的区别,因此您所能做的就是对所有键一视同仁,并决定您不太讨厌哪种故障模式。语言默认是假阴性比假阳性危害较小。如果您不同意,可以打开--noUncheckedIndexedAccess或添加| undefined手动添加到各个类型。但是,至少在 TS4.6 中,没有什么比这更好的了。

Playground 代码链接--noUncheckedIndexedAccess已关闭

Playground 代码链接--noUncheckedIndexedAccess已打开