总体而言,我对 Typescript 相处得很好,但我不断遇到这个问题。
假设我有一个类型,可以是一系列字符串之一:
type ResourceConstant = 'A' | 'B' | 'C';
Run Code Online (Sandbox Code Playgroud)
现在我想创建一个对象来保存每个资源的数量(因此是ResourceConstantto的映射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.
Run Code Online (Sandbox Code Playgroud)
那么什么是“映射对象类型”?我找到了一些有关“映射类型”的信息,但根本不清楚它们与此问题有何关系。也许他们的意思是我应该使用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.
Run Code Online (Sandbox Code Playgroud)
好的,所以它需要Partial:
let x: Partial<Record<ResourceConstant, number>> = {};
Run Code Online (Sandbox Code Playgroud)
这确实有效,但确实很可怕。
无论如何,让我们尝试在某个地方使用它:
for (let res in x) {
terminal.send(res, x[res]);
}
>>> error TS2345: Argument of type 'string' is not assignable to parameter of type 'ResourceConstant'.
Run Code Online (Sandbox Code Playgroud)
好的,所以它丢失了类型信息,因为对象总是按字符串索引。公平地说,我只是告诉 TypeScript 这绝对是一个 ResourceConstant:
for (let res: ResourceConstant in x) {
>>> error TS2404: The left-hand side of a 'for...in' statement cannot use a type annotation.
Run Code Online (Sandbox Code Playgroud)
也许我可以用来as强制类型:
for (let res as ResourceConstant in x) {
>>> error TS1005: ',' expected
Run Code Online (Sandbox Code Playgroud)
或者使用<>语法进行强制转换?
for (let res<ResourceConstant> in x) {
>>> error TS1005: ';' expected
Run Code Online (Sandbox Code Playgroud)
没有。看来我必须创建第二个中间变量来强制类型:
for (let res in x) {
let res2 = res as ResourceConstant;
terminal.send(res2, x[res2]);
}
Run Code Online (Sandbox Code Playgroud)
这可行,但也很可怕。
真是一团糟。我知道正确的答案是我应该使用new Map而不是{},这一切都很好,但这是 JS - 无论好坏,我们习惯于使用对象来完成这种事情。最重要的是,如果我注释现有代码库怎么办?当然,我不必Map为了 Typescript 的利益而将其全部更改?
为什么使用普通的物体看起来如此困难?我缺少什么?
microsoft/TypeScript#26797中完成了一些工作,允许使用任意属性键类型的索引签名。不幸的是,由于尚不清楚如何处理映射类型和索引签名行为之间的不匹配,因此它陷入了停滞。当前实现的索引签名与映射类型相比具有不同且更不健全的行为。您注意到这一点是因为您假设您可以分配{}给一个键为 的映射类型ResourceConstant,但由于缺少属性而被告知“否”。索引签名目前不关心属性是否丢失,这很方便但不安全。需要做一些工作来使映射类型和索引签名更加兼容才能继续。也许即将推出的迂腐索引签名 --noUncheckedIndexedAccess功能会解决这个问题?目前,您必须使用映射类型。
从语法上来说,从索引签名转换为映射类型非常简单:您可以将 from 更改{[k: SomeKeyType]: SomeValueType}为{[K in SomeKeyType]: SomeValueType}。Record<SomeKeyType, SomeValueType>这与实用程序类型相同。是的,如果你想省略一些键,你可以使用Partial<>.
有些人喜欢Partial<Record<K, T>>,因为它是对你正在做的事情的更“英语”的描述。不过,如果您觉得它很可怕,您可以通过自己编写映射类型来减少您的恐惧:
let x: { [K in ResourceConstant]?: number } = {};
Run Code Online (Sandbox Code Playgroud)
现在来说说for..in循环的东西。
for..in如果允许您在循环内注释迭代变量声明,那肯定会很好for..of。目前根本无法完成(有关更多信息,请参阅microsoft/TypeScript#3500。)
但让你注释resasResourceConstant会是一个问题。
这里最大的症结在于 TypeScript 中的对象类型是开放的(或可扩展的),而不是封闭的(或精确的)。像这样的对象类型{a: string, b: number}意味着“该对象string在键处有一个值属性a,在键处有一个number值属性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));
}
}
Run Code Online (Sandbox Code Playgroud)
然后没有什么可以阻止我这样做:
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!
Run Code Online (Sandbox Code Playgroud)
哎呀。我认为这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
}
}
Run Code Online (Sandbox Code Playgroud)
或这个:
// THIS IS THE RECOMMENDED SOLUTION HERE
for (let res of ["A", "B", "C"] as const) {
console.log((x[res] || 0).toFixed(2));
}
Run Code Online (Sandbox Code Playgroud)
或者,您可能不安全并使用类型断言或其他解决方法。明显的断言解决方法是创建一个新变量,如下所示:
for (let res in x) {
const res2 = res as ResourceConstant;
console.log((x[res2] || 0).toFixed(2));
}
Run Code Online (Sandbox Code Playgroud)
或者每次提到时都断言res,
for (let res in x) {
console.log((x[res as ResourceConstant] || 0).toFixed(2));
}
Run Code Online (Sandbox Code Playgroud)
但如果这太可怕了,那么您可以使用以下解决方法(来自此评论):
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));
}
Run Code Online (Sandbox Code Playgroud)
在这里,我res在外部作用域中声明了我想要的类型,然后在循环内使用它for..in。这显然没有错误。它仍然不安全,但也许您发现它比其他替代品更美味。
我认为你对 TypeScript 的失望是可以理解的……但我希望我已经传达了你所遇到的困难是有原因的。在映射类型和索引签名的情况下,墙恰好连接两个可用的走廊,但建筑师还不知道如何在其中切开一扇门而不使一侧或另一侧倒塌。在注释循环索引器的情况下,for..in它是为了防止人们掉进墙另一侧的露天坑中。
| 归档时间: |
|
| 查看次数: |
4309 次 |
| 最近记录: |