javascript - TypeScript 中可索引类型的多个索引器
我正在阅读 TypeScript 中的可索引类型。我正在查看这个例子。
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(超类、子类)这样的语言进行关联,将不胜感激。
答案#1
摘要:
- JS/TS 中的对象实际上并没有类型为
number的键;它们实际上是一种特殊类型的string键。 - TS 中的索引签名不能与其他索引签名或属性冲突。
- 由于“
number”键实际上是一种特殊类型的string键,因此如果存在,任何number索引签名属性都必须可以赋值给string索引签名属性。
一个可能令人困惑的问题是,JavaScript 并没有真正的数字键;在 JavaScript 中,键要么是 symbol,要么是 string……即使是像数组这样的东西通常被认为有数字索引,也不例外。
当你使用除 symbol 之外的任意类型的键来索引对象时,如果该键还不是 string 类型,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 的模板字面量类型实际上为你提供了一种表示这种类型的方法,如
type NumericString = `${number}`;
但在添加数字索引签名时它并不存在。因此,虽然一般情况下 number 不是 string 的子类型,但对于 键,你可以将 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 的意思是“如果你从 A 中读取一个类型为 string 的 任意键 的属性,你将得到一个类型为 string | number 的值。” foo 属性是兼容的,因为键 "foo" 是 string,且值类型 string 可以赋值给 string | number。 bar 属性是兼容的,因为键 "bar" 是 string,且值类型 number 可以赋值给 string | number。 但 baz 是错误的。键 "baz" 是字符串,但它违反了索引签名;boolean 不能赋值给 string | number。
你不能像上面那样使用索引签名来说“好吧,键 "baz" 的属性是 boolean 类型,但每个其他的 string 键属性的值类型是 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",是一个 string。NotOkay 的 string 索引签名表明每个 string 键的属性都将是 Dog 类型。但等等,number 索引签名与此不兼容。如果我继续将每个 string 索引的属性视为 Dog 类型,我将在某些这些所谓的 Dog 没有 breed 时遇到问题。
因此,number 索引签名因此是一个问题。把键类型 number 想象成 string 的一个特殊情况会有帮助,因此它的属性类型可以是 Dog 的一个特殊情况,而 Animal 是不可行的。
如果你将 Dog 和 Animal 调换过来,问题就消失了:
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 键的一种特殊情况,而 Dog 是 Animal 的一种特殊情况,所以一切运行得很顺利。你未知的 string 键的属性被认定为 Animal。将 Dog 当作 Animal 对待是没问题的,因为它本来就是 Animal。
代码的 Playground 链接
