typescript - 打字稿对象索引
问题描述
总体而言,我对 Typescript 的理解非常好,但我一直遇到这个问题。
假设我有一个类型可以是一系列字符串之一:
type ResourceConstant = 'A' | 'B' | 'C';
现在我想创建一个对象来保存每个资源的数量(所以映射ResourceConstant
到number
):
let x: { [key: ResourceConstant]: number } = {};
>>> error TS1337: An index signature parameter type cannot be a union type. Consider using a mapped object type instead.
那么什么是“映射对象类型”?我找到了一些关于“映射类型”的信息,但根本不清楚它们与这个问题有什么关系。也许他们的意思是我应该使用Record
:
let x: Record<ResourceConstant, number> = {};
>>> error TS2740: Type '{}' is missing the following properties from type 'Record<ResourceConstant, number>': U, L, K, Z, and 80 more.
好的,所以它需要Partial
:
let x: Partial<Record<ResourceConstant, number>> = {};
这行得通,但它真的很可怕。
无论如何,让我们尝试在某处使用它:
for (let res in x) {
terminal.send(res, x[res]);
}
>>> error TS2345: Argument of type 'string' is not assignable to parameter of type 'ResourceConstant'.
好的,所以它丢失了类型信息,因为对象总是由字符串索引。很公平,我只是告诉 TypeScript 这绝对是一个 ResourceConstant:
for (let res: ResourceConstant in x) {
>>> error TS2404: The left-hand side of a 'for...in' statement cannot use a type annotation.
也许我可以as
用来强制类型:
for (let res as ResourceConstant in x) {
>>> error TS1005: ',' expected
还是用<>
语法强制转换?
for (let res<ResourceConstant> in x) {
>>> error TS1005: ';' expected
没有。看来我必须创建第二个中间变量来强制类型:
for (let res in x) {
let res2 = res as ResourceConstant;
terminal.send(res2, x[res2]);
}
这行得通,但也很可怕。
真是一团糟。我知道正确的答案是我应该使用new Map
而不是{}
,这一切都很好,但这是 JS - 无论好坏,我们都习惯于使用对象来处理这种事情。最重要的是,如果我要注释现有的代码库怎么办?当然,我不应该Map
为了 Typescript 的利益而将其全部更改吗?
为什么使用普通对象似乎如此困难?我错过了什么?
解决方案
在microsoft/TypeScript#26797中完成了一些关于允许具有任意属性键类型的索引签名的工作。不幸的是,由于尚不清楚如何处理映射类型和索引签名行为之间的不匹配,因此它停滞了。当前实现的索引签名具有与映射类型不同且更不健全的行为。通过假设您可以分配{}
给键为 的映射类型ResourceConstant
,并且由于缺少属性而被告知不可以,您注意到了这一点。索引签名目前不关心是否缺少属性,这很方便但不安全。为了继续进行,需要做一些工作来使映射类型和索引签名更加兼容。也许即将到来的迂腐索引签名 --noUncheckedIndexedAccess
功能会解除阻止吗?现在,您必须使用映射类型。
从索引签名转换为映射类型在语法上很容易:您可以从 更改{[k: SomeKeyType]: SomeValueType}
为{[K in SomeKeyType]: SomeValueType}
. 这与Record<SomeKeyType, SomeValueType>
实用程序类型相同。是的,如果你想省略一些键,你可以使用Partial<>
.
有些人喜欢Partial<Record<K, T>>
它,因为它更像是对您正在做的事情的“英语”描述。尽管如此,如果你觉得它很可怕,你可以通过自己编写映射类型来减少你的恐惧:
let x: { [K in ResourceConstant]?: number } = {};
现在是for..in
循环的东西。
如果允许您在内部for..in
或for..of
循环中注释迭代变量声明,那肯定会很好。目前它根本无法完成(有关更多信息,请参阅microsoft/TypeScript#3500。)
但是让你注释res
会ResourceConstant
是一个问题。
这里最大的症结在于 TypeScript 中的对象类型是开放的或可扩展的,而不是封闭的或精确的。像这样的对象类型{a: string, b: number}
意味着“这个对象在键上有一个string
-valued 属性,在 key有a
一个-valuesnumber
属性b
”,但这并不意味着“并且没有其他属性”。不禁止额外的财产。所以像这样的值{a: "", b: 0, c: true}
是可分配的。(这很复杂,因为如果您尝试将具有额外属性的新对象字面量分配给变量,编译器会执行额外的属性检查,但这些检查很容易规避;有关更多信息,请参阅文档链接)。
因此,让我们假设我们可以for (let res: ResourceConstant in x)
在没有警告的情况下编写:
function acceptX(x: { [K in ResourceConstant]?: number }) {
for (res: ResourceConstant in x) { // imagine this worked
console.log((x[res] || 0).toFixed(2));
}
}
然后没有什么能阻止我这样做:
const y = { A: 1, B: 2, D: "four" };
acceptX(y); // no compiler warning, but this explodes at runtime
// 1.00, 2.00, and then EXPLOSION!
哎呀。我假设这for (let res in x)
只会迭代一些A
,B
和C
. 但是在运行时,aD
进去了,把一切都搞砸了。
这就是为什么他们不让你这样做。在没有确切类型的情况下,迭代“已知”类型的对象中碰巧出现的任何键都是不安全的。所以,你可以像这样安全:
for (let res in x) {
if (res === "A" || res === "B" || res === "C") {
console.log((x[res] || 0).toFixed(2)); // no error now
}
}
或这个:
// THIS IS THE RECOMMENDED SOLUTION HERE
for (let res of ["A", "B", "C"] as const) {
console.log((x[res] || 0).toFixed(2));
}
或者,您可能不安全并使用类型断言或其他解决方法。明显的断言解决方法是创建一个像这样的新变量:
for (let res in x) {
const res2 = res as ResourceConstant;
console.log((x[res2] || 0).toFixed(2));
}
或者每次提到时都断言res
,
for (let res in x) {
console.log((x[res as ResourceConstant] || 0).toFixed(2));
}
但如果这太可怕了,那么您可以使用以下解决方法(来自此评论):
let res: ResourceConstant; // declare variable in outer scope
for (res in x) { // res in here uses same scope
console.log((x[res] || 0).toFixed(2));
}
在这里,我res
在外部范围内声明为我想要的类型,然后在for..in
循环中使用它。这显然没有错误。它仍然不安全,但也许你觉得它比其他替代品更可口。
我认为您对 TypeScript 的挫败感是可以理解的……但我希望我已经传达了您遇到的障碍是有原因的。在映射类型和索引签名的情况下,这堵墙碰巧连接了两个可用的走廊,但建筑师们还不知道如何在其中切一扇门而不让一侧或另一侧倒塌。在注释for..in
循环索引器的情况下,它是为了阻止人们掉入那堵墙另一侧的露天坑。
推荐阅读
- java - Ksoap on android raed 来自 ac# web 服务的对象列表
- java - 如果字符串以 (aioeuAIOEU) 之类的元音结尾,我该怎么办?
- angular - 在 Angular 6 中设置重复定时任务的更好(高效/干净)方式是哪一种?
- reactjs - 包含数组的 API 响应的 TypeScript 声明
- git - 是否可以配置默认或全局 git 钩子?
- mongodb - 在 mongodb 文档的嵌套数组中查找状态总和
- html - 表和绝对表,滚动不一样
- .htaccess - 重写一个被.htaccess美化的重写的url
- python - 如何读取存储在 Python Pandas 中本地的 ORC 文件?
- excel - 如何仅在存在时禁用数据透视项目?