TypeScript 中可索引类型中的多个索引器

Multiple indexer in Indexable types in TypeScript

我正在用 TypeScript 阅读 Indexable Types。我正在看这个例子。

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
  [x: number]: Animal;
// Numeric index type 'Animal' is not assignable to string index type 'Dog'.
  [x: string]: Dog;
}

我无法理解为什么上面的代码会产生问题。我在网上浏览了很多文章,但无济于事。谁能用至少一个例子用简单的语言向我解释上面的代码如何在这样做时会产生问题。

interface Okay {
      [x: string]: Dog;
      [x: number]: Animal;
}

如果与 C++、Java(超类、子类)等语言的相关性也在这里制作,将不胜感激。

长话短说:

  • JS/TS 中的对象实际上没有 number 类型的键;它们实际上是一种特殊类型的 string 键。
  • TS 中的索引签名不能与其他索引签名或属性冲突。
  • 由于“number”键实际上是一种特殊的 string 键,任何 number 索引签名 属性 必须可分配给 string 索引签名 属性 如果存在。

一个可能令人困惑的问题是 JavaScript 没有真正的数字键; JavaScript 中的键是 symbols 或 strings... 即使对于通常被认为具有数字索引的数组之类的东西也是如此。

当您使用 symbol 以外的任何类型的键对对象进行索引时,JS 将 将其强制转换为 string 如果它不是已经一个了。因此,虽然您可以想到在索引 0 处有一个元素的单元素数组,但在技术上更正确的说法是它在索引 "0":

处有一个元素
const arr: [string] = [""];
const arrKeys: string[] = Object.keys(arr);
console.log(arrKeys) // ["0"]
console.log(arrKeys.includes(0 as any)) // false
console.log(arrKeys.includes("0")) // true
arr[0] = "foo";
arr["0"] = "bar";
console.log(arr[0]); // "bar";

TypeScript 允许您指定键类型 number 的索引签名来表示类似数组的元素访问,但它不会改变数字键被强制转换为字符串的事实。如果 number 索引签名改用 NumericString 之类的类型会更准确。但是在 TypeScript 中没有公开这样的类型。 (另外:好吧,TypeScript 4.1 的 template literal types 实际上给了你一种方法来表示这样的类型

type NumericString = `${number}`;

但在添加数字索引签名时它不存在。)所以虽然一般来说 number 不是 string 的子类型,因为 keys,您可以将 number 视为 string.

的一种类型

下一个可能的混淆点是 TypeScript 不将索引签名视为“异常”。如果添加索引签名,它不得与任何其他属性冲突。 (有关详细信息,请参阅 。)例如,下面的类型的 baz 成员有问题:

interface A {
  foo: string; // okay
  bar: number; // okay
  baz: boolean; // <-- error! boolean not assignable to string | number
  [k: string]: string | number;
}

索引签名 [k: string]: string | number 表示“如果您使用 类型 string 的任何键 A 读取 属性 ,您将获得 string | number 类型的值。” foo 属性 是兼容的,因为键 "foo"string,值类型 string 可分配给 string | numberbar 属性 是兼容的,因为键 "bar" 是一个 string,值类型 number 可以分配给 string | number。但是 baz 是错误的。 key "baz" 是一个字符串,但是违反了索引签名; boolean 不可分配给 string | number

你不能像上面那样使用索引签名说“好吧,键 "baz" 处的 属性 是一个 boolean 但每个 other string-keyed 属性 有一个 string | number 类型的值。最好有一种方式来表达这一点,(参见 microsoft/TypeScript#17687 的请求)但是索引签名不能那样工作。

有助于认为键 "baz"string 的特例,因此它的 属性 类型可以是 string | number 的特例,并且 boolean 不起作用。


所以,让我们把它们放在一起:

interface NotOkay {
  [x: number]: Animal; // error!  Animal not assignable to Dog
  [x: string]: Dog;
}

假设我有一个类型为 NotOkay 的值,我使用其中一个键对它进行索引:

const notOkay: NotOkay = {
  str: dog,
  123: animal
}
const randomKey = Object.keys(notOkay)[Math.random() < 0.5 ? 0 : 1]; // string
const randomProp = notOkay[randomKey] // Dog?
console.log(randomProp.breed.toUpperCase()); // either LAB or runtime error?

这可能会导致运行时错误,因为键 123 实际上是 "123",一个 stringNotOkaystring 索引签名表示 string 键上的每个 属性 都是 Dog 类型。但是等等,number 索引签名与那个不兼容。如果我继续将每个 string 索引的 属性 视为类型 Dog,我将遇到其中一些假定的 Dog 没有 breed 的问题.

因此 number 索引签名是一个问题。有助于认为键类型 numberstring 的特例,因此它的 属性 类型可以是 Dog 的特例,而 Animal不起作用。


如果切换 DogAnimal,问题就会消失:

interface Okay {
      [x: string]: Animal;
      [x: number]: Dog;
}
const okay: Okay = {
  str: animal,
  123: dog
}
const randomKey2 = Object.keys(okay)[Math.random() < 0.5 ? 0 : 1]; // string
const randomProp2 = okay[randomKey2] // Animal
console.log(randomProp2.name.toUpperCase()); // no error here, FIDO or FLUFFY

因为 number 键是 string 键的特例,而 DogAnimal 的特例,所以一切正常。您的 属性 的未知 string 密钥已知为 Animal。如果您像对待 Animal 一样对待 Dog 也没关系,因为它是一个。


Playground link to code