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 中的键是 symbol
s 或 string
s... 即使对于通常被认为具有数字索引的数组之类的东西也是如此。
当您使用 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 | number
。 bar
属性 是兼容的,因为键 "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"
,一个 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
。如果您像对待 Animal
一样对待 Dog
也没关系,因为它是一个。
我正在用 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 中的键是 symbol
s 或 string
s... 即使对于通常被认为具有数字索引的数组之类的东西也是如此。
当您使用 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 | number
。 bar
属性 是兼容的,因为键 "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"
,一个 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
。如果您像对待 Animal
一样对待 Dog
也没关系,因为它是一个。