我尝试了一些TS索引签名的例子,感觉键的限制行为非常不一致。
const s = Symbol();
type DictNumber = {
[key: number]: string;
}
const dictNumber: DictNumber ={
1: 'andy', // no error, which is what I defined.
'foo': 'bar', // Error, because the key is string not a number.
[s]: 'baz', // no error, but why ??? [s] is a symbol not a number right?
}
type DictString = {
[key: string]: string;
}
const dictString: DictString={
1: 'andy', // no error? why? is it converted from number to string for me?
'foo': 'bar', // no error, which is what I defined.
[s]: 'baz', // no error, but why ??? [s] is a symbol not a string right?
}
type DictSymbol= {
[key: symbol]: string;
}
const dictSymbol: DictSymbol={
1: 'andy', // Error, because the key is number not a symbol.
'foo': 'bar', // no error, why?
[s]: 'baz', // no error, which is what I defined.
我打开noImplicitAny
和alwaysStrict
。这里是操场
我可能错过了一些非常基本的东西,有人能解释一下为什么会发生这种情况吗?
它比表面看起来更有趣。想想看:
const s = Symbol();
type DictNumber = {
[key: number]: string;
}
const dictNumber: DictNumber = {
1: 'andy', // ok
'foo': 'bar', // error
[s]: 'baz', // ok
}
但尝试修复foo
键:
const dictNumber: DictNumber = {
1: 'andy', // ok
'2': 'bar', // ok
[s]: 'baz', // error
}
所以,现在我们知道,有一个错误,但由于错误高亮算法,它没有高亮显示,因为有另一个错误抛出更快。或者直接替换它们:
const dictNumber: DictNumber = {
1: 'andy', // ok
[s]: 'baz', // error
'foo': 'bar', // ok
}
我理解TS团队为什么这么做,为了性能。如果已经有错误,则无需验证其他键。
与const dictSymbol: DictSymbol
相同。它可以工作,尝试替换它们。
只有DictString
不符合我们的期望。根本没有错误。
const s = Symbol();
type DictSymbol = Record<symbol, string>
type DictString = Record<string, string>
let dictString: DictString = {
a: 'baz',
}
let dictSymbol: DictString = {
[s]: 'baz', // no error , but should be
}
dictString = dictSymbol // ok
dictSymbol = dictString // ok
这是因为,根据我的经验,Record<K,V>
比interface
表示更不安全。因为类型别名是默认索引的。请看我的回答。
为了确保安全,只需使用interface
:
const s = Symbol();
interface DictSymbol {
[sym: symbol]: string
}
type DictString = Record<string, string>
let dictString: DictSymbol = {
a: 'baz', // error
}
let dictSymbol: DictString = {
[s]: 'baz', // no error , but should be
}
dictString = dictSymbol // ok
dictSymbol = dictString // error
如果你把DictString
转换成interface
,你会得到更多的错误:
const s = Symbol();
interface DictSymbol {
[sym: symbol]: string
}
interface DictString {
[str: string]: string
}
let dictString: DictSymbol = {
a: 'baz', // error
}
let dictSymbol: DictString = {
[s]: 'baz', // still no error
}
dictString = dictSymbol // ok
dictSymbol = dictString // error
我不知道为什么这里没有错误。我希望
let dictSymbol: DictString = {
[s]: 'baz', // still no error
}
这里有很多问题。
具有类似
的索引签名type DictNumber = {
[key: number]: string;
}
表示每个数值属性必须具有string
类型。但是非数字属性的类型完全不受限制。这意味着这是好的:
const a = {
0: "123",
abc: 123, // non-numeric property of number type
[s]: 123 // symbol property of number type
}
const b: DictNumber = a // Ok
编辑:正如来自乌克兰的@captain-yossarian在他的回答中指出的,有一个多余的财产检查错误。至少使用数字索引签名
您期望的错误主要依赖于多余的属性检查。是的,当你把对象字面值赋值给一个显式类型的变量时,TypeScript确实会执行多余的属性检查。
const dictNumber: DictNumber = {
1: 'andy',
'foo': 'bar', // Error, excess property check
}
但是目前有一个bug,如#44794所述。当索引签名存在时,编译器不会在多余的属性检查中检测symbol
-properties。
const dictString: DictString = {
1: 'andy',
[s]: 'baz', // No Error, but there should be
}
这是无意的行为,可能会在将来的某个时候修复。
最后:
const dictString: DictString = {
1: 'andy'
}
索引时,数字被强制转换为字符串。这就是为什么当string
索引签名存在时,数字被允许作为属性。
TL;DR
JS对象不是字典,你正在寻找Map
长回答
符号保证是唯一的,因此不是普通的键,它们是作为一种存储对象上的半私有(它们可以通过Object.getOwnPropertySymbols()
访问)属性的方式而存在的,而不会与其他键冲突。这是一种弱封装。
TypeScript知道符号不会干扰其他对象的键,因此允许它们被用作键。
因为JavaScript对象不像Python和c#那样是字典,所以你可以在同一个对象中混合特殊键。TypeScript是JavaScript的超集,必须支持这种行为,你可以为任何对象添加私有属性,而不管键的类型。
此外,因为对象已经有了原生的Symbol键,所以你可以添加更多的符号键。
对于这种行为的合理解释,JS的答案通常是"因为它很奇怪"。
代替"dict/dictionary"的更好的术语;是"record"它也是TypeScript中内置的实用工具类型。