首页 > 解决方案 > 打字稿对象索引

问题描述

总体而言,我对 Typescript 的理解非常好,但我一直遇到这个问题。

假设我有一个类型可以是一系列字符串之一:

type ResourceConstant = 'A' | 'B' | 'C';

现在我想创建一个对象来保存每个资源的数量(所以映射ResourceConstantnumber):

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 的利益而将其全部更改吗?

为什么使用普通对象似乎如此困难?我错过了什么?

标签: 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..infor..of循环中注释迭代变量声明,那肯定会很好。目前它根本无法完成(有关更多信息,请参阅microsoft/TypeScript#3500。)

但是让你注释resResourceConstant是一个问题。

这里最大的症结在于 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,BC. 但是在运行时,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循环索引器的情况下,它是为了阻止人们掉入那堵墙另一侧的露天坑。

Playground 代码链接


推荐阅读