网络数据不能只相信 TypeScript 类型
网络数据不能只相信 TypeScript 类型
为接口响应写好 TypeScript 类型后,编辑器能补全字段,编译器也能检查使用方式。但这些类型不会随着请求发送到服务端,也不会在 JSON 进入浏览器时自动运行。只要真实响应与声明不同,错误仍然会在后面的业务代码里出现。
网络响应、URL 参数、localStorage、跨窗口消息和第三方脚本数据都来自运行时边界。类型断言只能告诉编译器“把它当成某种类型”,不能证明数据确实符合这个结构。
校验应该发生在数据进入系统时
运行时校验负责把未知输入转换成应用可以依赖的数据,或者明确拒绝它。越靠近边界完成这件事,后面的代码越不需要反复判断同一字段是否存在、类型是否正确。
结构校验和业务校验不是一回事。确认金额是字符串、时间字段存在,属于数据能否被解析;判断余额是否足够、订单能否取消,则属于领域规则。前者可以由 Schema 工具统一处理,后者应该留在拥有业务事实的模块或服务端。
不是每个响应都需要逐字段进行同等强度的检查。内部且有稳定契约的接口,可以在关键边界校验;第三方数据、持久化旧数据和影响资金或权限的响应则更值得严格处理。校验成本应放在风险高、错误后果大的位置。
校验失败后也要保留上下文。接口名称、字段路径、预期类型、实际值类别和请求 ID 都有助于排查,但日志里不应原样记录令牌、密码和个人信息。
解析后最好转换成应用自己的模型
服务端为了兼容传输,可能把时间、金额和枚举都表示为字符串。页面如果到处直接使用传输模型,每个组件都会重复解析,并可能采用不同规则。
更清楚的边界是先校验响应,再把它转换成应用模型。例如把时间字符串解析为明确的时间表示,把缺失的可选集合补成空集合,或者将服务端字段名映射成领域里更自然的名称。转换完成后,内部代码只依赖一种稳定形状。
不过,默认值不能掩盖异常。服务端漏传价格时补成 0,会把契约错误伪装成免费;未知订单状态被当成普通状态,也可能让页面开放错误操作。只有业务上确实等价时,才适合在边界补默认值。
JSON 能表示的值比 JavaScript 少
JSON 只有对象、数组、字符串、数字、布尔值和 null。Date 会变成字符串,undefined 不能作为普通值保留,BigInt 也不能直接交给标准 JSON 序列化。NaN 和无穷大同样不属于 JSON 数字。
这意味着序列化前后不一定保持 JavaScript 值的原貌。接口要明确每种特殊数据怎样表示,而不是让客户端从字段名猜测。日期、十进制金额、大整数 ID 和二进制数据尤其需要统一约定。
循环引用也无法用 JSON 表达。传输模型应该是可序列化的数据结构,不适合直接把包含方法、类实例和复杂对象图的运行时对象交给请求层。
数字和金额要先约定精度
JavaScript 的 number 使用双精度浮点数,超过安全整数范围后不能准确表示每一个整数。数据库里的大整数主键如果直接作为 JSON 数字返回,浏览器可能在还没显示之前就已经改变了它。
这类标识符更适合作为字符串传输。金额也不应依赖二进制浮点数进行精确计算,可以按最小货币单位传整数,或使用十进制字符串和对应的十进制运算工具。选择哪种方式不重要,前后端使用同一种规则才重要。
反序列化时不要用宽松转换静默接受脏值。空字符串、带单位的文本和超出范围的数字是否允许,都应该由字段契约决定。
时间需要带上时区语义
一个没有时区的日期时间字符串无法说明它表示用户本地时间、服务器时间还是 UTC。不同运行环境解析它时可能得到不同结果。
表示时间点时,更适合传输带偏移量的 ISO 8601 字符串或明确约定 UTC;表示生日、结算日这类日历日期时,则不应该随时区换算成前一天或后一天。时间点、日期和时长是不同类型,不能只因为它们都能写成字符串就共用一套解析逻辑。
格式正确也不代表业务有效。结束时间早于开始时间、预约日期落在不可用区间,仍然需要领域校验。
缺失、null 和未知枚举不是同一种状态
字段缺失可能表示旧版本没有提供,null 可能表示服务端明确知道它没有值。空字符串和空数组又有各自含义。契约需要把这些情况区分开,客户端也不应使用一个宽泛的假值判断全部合并。
枚举会随着服务端演进增加新成员。前端如果假设当前联合类型已经覆盖未来所有值,旧版本应用遇到新状态时可能直接进入错误分支。边界层可以保留一个未知状态,并限制用户执行依赖精确业务含义的操作。
新增字段通常可以被旧客户端忽略,删除字段和改变含义则更危险。需要同时支持多种结构时,可以用明确版本或可辨识字段选择对应解析器,避免用一串模糊的可选字段猜测数据属于哪个版本。
查询参数和表单也需要序列化规则
HTTP 数据不只有 JSON。查询参数中的数组可以用重复键、逗号分隔或其他形式表达;布尔值可能使用 true、1 或字段是否出现表示。如果前后端没有约定,同一筛选条件很容易被解析成不同结果。
表单上传则要区分文本字段和文件,不要手工覆盖运行时生成的 multipart 边界。空字段是否省略、文件大小和媒体类型怎样限制,也属于接口契约。
这些规则应该与 API 契约与类型生成保持一致。类型生成负责减少重复声明,运行时校验负责确认实际输入,序列化约定则保证发送和接收双方对同一份数据有相同理解。