更新说明: TypeScript 4.1 引入了
noUncheckedIndexedAccess
编译选项,可以在一定程度上缓解本文讨论的问题。启用此选项后,Index Signature 访问会自动包含| undefined
类型。
TypeScript 中的 Index Signature 使用频率很高。它可以用来表示通过方括号[]
访问属性的数据结构,例如 TypeScript 内建类型Array<T>
中就包含如下实现:
1 | interface Array<T> { |
同时,在 JavaScript 中相比 Map 类的实例,开发者更倾向于直接使用 object 来实现映射数据结构。虽然 Map 在某些方面更安全,但它存在序列化问题——JSON.stringify()
无法直接序列化 Map 对象,这在前后端数据交互中带来不便。TypeScript 将对象映射称为 Record,其类型定义为type Record<K extends keyof any, T> = { [P in K]: T }
。
然而,如果期望通过 TypeScript 实现完全的类型安全,需要谨慎使用 Index Signature。主要问题在于使用 Index Signature 获取的值类型并不准确,例如
1 | const map: { [key: string]: number } = { |
这里使用不存在的 key 获取到的结果是undefined
,但 TypeScript 推导出的值类型并不包含这种可能性。在 web 应用中经常使用 array 和 map 来批量处理数据或映射数据结构,这种隐藏的undefined
可能导致运行时错误。
早在 2017 年初,社区就在 microsoft/TypeScript 提出了相关 issue。有人提出直接修改 Index Signature 的值定义,加上| undefined
。但这个方案存在问题:在定义 map 时,我们只希望在获取值时类型能够表示值可能不存在,而在值类型定义后加上| undefined
会改变语义,也无法阻止往集合中添加undefined
值。更重要的是,这会影响迭代器的返回值类型——例如 Object.values()
的返回类型也会包含 undefined
,这显然不符合预期。
这个问题至今未得到解决,根本原因在于 TypeScript 依然需要保持对 JavaScript 代码的完全兼容。JavaScript 的一些设计限制势必会影响 TypeScript 构建更安全的类型系统。这不仅仅体现在 Index Signatures 上,而是一个更广泛的架构约束。
作为静态类型的 superset,TypeScript 的语法设计不能与 JavaScript 产生冲突,这意味着它无法限制 JavaScript 的某些功能,也无法在运行时做任何额外的事情。而彻底解决 Index Signatures 的类型安全问题,恰好需要突破这些根本性的限制。
我时常想,如果脱离了 JavaScript 的约束,TypeScript 或许能成为一门更优雅的语言,但也会失去其最大的优势——渐进式采用和庞大的生态系统。现在的设计更多是现实考量下的权衡取舍。
当然,面对这样的现实,不同的开发者有不同的选择:有人选择妥协,有人通过各种变通方案维护内心的代码洁癖,也有人在心里默默期盼——愿世界没有 JS。