typescript索引签名键限制



我尝试了一些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.

我打开noImplicitAnyalwaysStrict。这里是操场

我可能错过了一些非常基本的东西,有人能解释一下为什么会发生这种情况吗?

它比表面看起来更有趣。想想看:

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中内置的实用工具类型。

最新更新