打字稿对象索引

时间:2020-09-24 00:24:42

标签: 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吗?

为什么使用普通对象似乎如此困难?我想念什么?

1 个答案:

答案 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..infor..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)仅会迭代ABC中的一些。但是在运行时,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循环索引器的情况下,这是为了防止人们掉入该墙另一侧的露天坑中。

Playground link to code