TL;DR:
- 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 不会将索引签名视为“例外”。如果您添加索引签名,则它不得与任何其他属性冲突。 (有关详细信息,请参阅 this q/a。)例如,以下类型的 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 | number。 bar 属性是兼容的,因为键 "bar" 是 string,而值类型 number 可分配给 string | number。但是baz 是错误的。键"baz"是字符串,但是违反了索引签名; boolean 不能分配给 string | number。
你不能使用像上面这样的索引签名来表示“好吧,键 "baz" 处的属性是 boolean 但每个 other string-keyed 属性都有一个 @ 类型的值987654373@。如果能有这样的说法就好了(请参阅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-indexed 属性都视为Dog 类型,我会遇到一些假设Dogs 没有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 对待,没关系,因为它是一个。
Playground link to code