TypeScript 能在低级错误进入生产环境之前将其拦截。它会针对拼写错误的属性、遗漏的参数以及返回形状错误的逻辑分支向你发出警告。你修复它,构建通过,然后发布。但静态类型有一个硬性限制。一旦编译器完成工作,所有的注解都会被剥离。运行你代码的 JavaScript 引擎从未听说过你的 interface、品牌类型(branded types)或精心约束的字符串字面量。它只认识值和语言本身的实际规则。
构建通过并不意味着运行时安全。测试通过并不意味着用户看到的是一个稳定的应用程序。如果你的思维模型止步于 TypeScript 的边界,那么在真正发生崩溃的地方,你实际上是在盲目飞行。
编译时的幻象
TypeScript 的整个类型系统在编译期间会被擦除。打开任何项目的编译后 JavaScript 输出,你会发现找不到 interface、type 或泛型约束的任何痕迹。它们只是设计时的脚手架。浏览器或 Node.js 进程执行的是纯 JavaScript,而流经你函数的数值并不保证与你在纸面上声明的类型相匹配。
这种差距在系统的边缘处表现得最为明显。网络响应、用户输入和第三方库可能会注入违反你类型定义的数值。如果你声明了一个变量为 strictEmail: string,但如果通过不受信任的 API 进入了错误数据,它在运行时仍可能是一个数字。TypeScript 无法跟随你的代码进入生产环境去强制执行任何规则。运行时运行在一个完全不同的平面上,将这两个平面混为一谈会导致静态分析永远无法捕捉到的故障。
当数字背叛你时
TypeScript 看到的是一个 number。JavaScript 引擎看到的则是 IEEE 754 双精度浮点数。这种区别在造成灾难性后果之前看似无害。
JavaScript 为每个数字分配 64 位,但只有 53 位用于存储尾数(mantissa)。这产生了一个安全整数上限:9,007,199,254,740,991。任何大于此值的数字都会被舍入到最接近的可表示值。在实践中,两个真正不同的标识符可能会在你的应用程序内部塌缩成同一个值。
Snowflake ID 和其他分布式 64 位整数标识符经常会超过这个限制。追踪高额小额货币单位的金融系统也可能触及这一界限。危险往往在你的业务逻辑运行之前就已经出现了:JSON.parse 会急切地将 Payload 中的数字字面量转换为 JavaScript 数字,在到达时便悄无声息地截断了精度。你的类型定义可能承诺了 id: number,但在第一次函数调用之前,运行时值就已经损坏了。
解决方法很直接,但需要整个技术栈的严谨性。在网络传输过程中,将大型标识符保持为字符串。在你的 JSON Schema 和 API 合约中,将这些字段定义为字符串而非数字。如果你必须对超出安全范围的值进行算术运算,请使用 BigInt。但要小心:BigInt 不能与标准的 JavaScript 数字隐式混合使用,而且除非你显式将其转换回字符串,否则 JSON.stringify 在序列化 BigInt 时会抛出错误。默认情况下,请将 ID 视为不透明的令牌(opaque tokens)。只有当你处于真正需要的隔离计算模块中时,才将其解析为数值形式。
