总体来说我对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
吗?
为什么使用普通对象似乎如此困难?我想念什么?
答案 0 :(得分:1)
microsoft/TypeScript#26797中做了一些工作,以允许使用任意属性键类型的索引签名。遗憾的是stalled,因为尚不清楚如何处理映射类型和索引签名行为之间的不匹配。当前实现的索引签名与映射的类型相比,具有不同的行为,并且声音行为不健全。通过假设您可以将{}
分配给键为ResourceConstant
的映射类型,并且由于缺少属性而被告知否,您注意到了这一点。索引签名目前不关心属性是否丢失,这很方便但不安全。为了继续进行映射的类型和索引签名的兼容性,需要做一些工作。即将推出的pedantic index signatures --noUncheckedIndexedAccess
功能可能会阻止此功能吗?现在,您必须使用映射类型。
从索引签名到映射类型的转换在语法上很容易:您可以从{[k: SomeKeyType]: SomeValueType}
更改为{[K in SomeKeyType]: SomeValueType}
。这与Record<SomeKeyType, SomeValueType>
utility type相同。是的,如果您想省略一些键,可以使用Partial<>
。
有些人喜欢Partial<Record<K, T>>
,因为它是您所做工作的更像“英语”的描述。不过,如果您发现?很恐怖,可以自己编写映射类型来减轻恐怖:
let x: { [K in ResourceConstant]?: number } = {};
现在是for..in
循环的东西。
如果允许您对for..in
或for..of
循环中的迭代变量声明进行注释,那绝对好。当前,它根本无法完成(有关更多信息,请参见microsoft/TypeScript#3500。)
但是让您将res
注释为ResourceConstant
会是一个问题。
这里的最大障碍是TypeScript中的对象类型是 open 或可扩展的,而不是 closed 或exact。像{a: string, b: number}
这样的对象类型表示“此对象在键string
处具有a
值的属性,在键number
处具有b
值的属性”,但它不是不是的意思是“并且没有其他属性”。禁止额外的属性。因此,可以分配{a: "", b: 0, c: true}
之类的值。 (这很复杂,因为如果您尝试将具有额外属性的新鲜对象常量分配给变量,则编译器确实会执行excess property checking,但这些检查很容易绕开;有关更多信息,请参见文档链接。)
因此,让我们想象一下,我们可以写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
中的一些。但是在运行时,D
进入了那里,搞砸了一切。
这就是为什么他们不允许您这样做。在没有确切类型的情况下,迭代“已知”类型对象中的任何键只是不安全的。因此,您可以像这样安全:
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));
}
或者,您可能不安全,请使用type assertion或其他解决方法。明显的断言解决方法是创建一个新变量,如下所示:
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));
}
但是如果这太可怕了,那么您可以使用以下解决方法(来自this comment):
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
循环索引器的情况下,这是为了防止人们掉入该墙另一侧的露天坑中。