Mapped types 类似于 the Record<K, V> utility type 映射 string 和 number literal 各个属性的键,因此 Record<"A" | "B", string> 等同于 {a: string; b: string}。
但是诸如string 本身或number 之类的宽非文字类型的键,或`foo${string}` 之类的模式模板文字类型(在microsoft/TypeScript#40598 中实现)被映射到index signatures。来自索引签名的文档:
有时您无法提前知道类型属性的所有名称,但您确实知道值的形状。在这些情况下,您可以使用索引签名来描述可能值的类型。
因此,索引签名并不真正代表具有相关类型的所有可能键的“完整对象”,例如所有单键对象{a: string} & {b: string} & {c: string} & ... & {foo: string} & ... {blahblah: string} & ... 的无限intersection。
(旁白:你说一个完整的对象将等同于never,因为这是不可能的。但这并不准确。一个Proxy 对象可以很容易地符合这种类型。即使它 在 JavaScript 中是不可能的,如果没有某种关于无穷大的明确公理,你会希望类型系统将其视为 never,这并不明显,然后你会必须弄清楚如何在不禁止递归数据类型的情况下做到这一点。)
无论如何,索引签名更像是属性上的约束。 {[k: IndexType]: ValType} 形式的索引签名意味着“如果该对象具有IndexType 类型的属性键,那么这样的属性将具有@987654350 类型的值@"。从某种意义上来说,更像是所有单键对象与optional properties的无限交集,比如{a?: string} & {b?: string} & {c?: string} & ... & {foo?: string} & ... {blahblah?: string} & ...
当然比这更复杂,因为编译器在传统上并未将索引签名和可选属性视为相同。
在 TypeScript 4.1 之前,索引签名总是可以让你读取属性并获得一个值,即使我刚刚解释完它们更像是可选属性。对此有很多抱怨,因此 TypeScript 4.1 引入了the --noUncheckedIndexedAccess compiler flag,它在读取时将undefined 添加到索引签名属性值的域中,但在写入时不添加。即使使用--strict,默认情况下也不会启用它,因为虽然它的类型更安全,但在人们通过数组或对象索引的任何情况下都会很烦人......像for (let i=0; i<arr.length; i++) {arr[i]}或Object.keys(obj).forEach(k => obj[k])这样的代码应该在技术上显示arr[i] 和obj[k] 可能是undefined,至少没有办法跟踪i 和k 的身份 而不仅仅是类型.
在 TypeScript 4.4 之前,可选属性在阅读和写作时都被视为将undefined 作为其域的一部分。人们对此也有很多抱怨,因此 TypeScript 4.4 引入了the --exactOptionalPropertyTypes compiler flag,它在读取时保留了undefined,但拒绝写入带有undefined 的属性。 --strict 也没有包含此内容,因为如果 bar 是可选的,则类似 foo.bar = foo.bar 的内容现在被视为错误。
如果您启用这两个编译器标志,则索引签名和可选属性具有相似的行为,尽管我确信存在更多边缘情况。
无论如何...Record<string, string> 等同于{[k: string]: string}) 而Record<never, string> 等同于empty object type {}。它们不是相同的类型,但由于与microsoft/TypeScript#7029 中实现的隐式索引签名相关的规则,它们是相互兼容的。
那里也有很多东西要解开,关于weak type detection、excess property checking,以及索引签名和interface 类型之间的交互(参见microsoft/TypeScript#15300)。不过,我现在要停下来了,因为这个答案已经够长了。