用联合类型、泛型和类型收窄表达业务约束,减少无效状态。
核心思路
优先让类型从数据和函数返回值推导,公共边界再显式声明。使用判别联合表达状态机,避免多个布尔值产生不可能组合。
落地步骤
外部输入永远需要运行时校验,类型断言不能替代验证。复杂类型若难以解释,应回到更简单的数据模型。
验证清单
实施后不要只确认“能运行”,还应检查错误率、延迟、资源占用和异常路径。把验证命令、预期结果和回滚方式写进变更记录,后续维护会轻松很多。
小结
好的工程实践不是堆叠工具,而是用清晰边界、可观察结果和可逆变更降低长期成本。先解决最重要的问题,再逐步自动化。
可执行实践
用判别联合表达加载、成功和失败状态,用泛型约束容器而不是返回 any。API 边界通过运行时 schema 验证后再转换成领域类型。
验证与退出条件
开启 strict 并逐步减少断言。为非法状态写 never 穷尽检查,类型复杂到无法解释时优先简化数据模型而不是继续堆条件类型。
实践场景
以“为外部 API 响应建立可靠类型边界”为例。实施前先保存当前版本、配置和关键指标基线,写明成功条件与停止条件;涉及生产数据或外部写入时,先在隔离环境验证。
分步实施
unknown 数据先由运行时 schema 校验,再转换为领域类型;用判别联合表达状态,避免 any 和非空断言扩散。每完成一步都记录可观察结果,不把命令返回零等同于业务正确。
故障演练
构造缺字段、额外字段、旧版本和未知枚举值,确保错误在入口被分类而非在深层崩溃。演练应使用可清理的测试数据,并验证告警、日志和恢复路径,而不只是确认系统最终又能访问。
观测与回滚
观察校验失败和未知状态;兼容变更先扩读后改写,必要时恢复旧解码器。变更单中应写清负责人、观察窗口、阈值和回滚触发器,避免故障发生后再临时争论。
完成后把使用的版本、关键命令、观测窗口和结果写入变更记录。没有验证证据的“已完成”不能作为上线结论。
第二轮实践复核
可复用交付物: 运行时 schema、领域类型和边界失败样本。复用时先固定软件版本、依赖锁文件、测试数据规模和运行环境;文章中的命令包含占位符,必须替换成自己的非生产目标后再执行。
最低验证: npx tsc --noEmit && npm test
执行记录要保存命令、退出码、关键输出、观察窗口和回滚结果。若验证会产生写入,先使用隔离环境或 dry-run;任何失败都不能通过扩大权限、关闭校验或跳过测试来“解决”。
完整实战:让订单状态在编译期闭合
类型的价值不是把 JavaScript 改写成更多接口,而是让非法状态更难出现。订单查询可能返回加载中、成功、失败和空结果;如果用四个可选字段表达,调用方会遇到 loading=true 同时带 error 的矛盾组合。
1 | type Order = Readonly<{ |
当以后新增 cancelled 分支,assertNever 会让漏改的消费方在编译时暴露。不要用 as LoadState<Order> 绕过构造错误;为每个分支提供显式工厂,或者让解析器返回判别联合。
外部输入仍需运行时校验
类型声明在编译后不会保护网络响应、环境变量和本地存储。下面的 unknown 迫使边界代码先缩窄类型:
1 | function isOrder(value: unknown): value is Order { |
大型项目通常用 schema 库生成解析器或从 OpenAPI 生成类型,但仍要测试“服务端新增字段、缺字段、错误枚举、超大数字和 null”这些边界。生成代码要固定工具版本,并在 CI 检查契约漂移。
| 味道 | 隐患 | 改进方向 |
|---|---|---|
大量 any |
错误跨层扩散 | 边界用 unknown,验证后缩窄 |
到处 as |
编译器被迫相信调用方 | 工厂函数、类型守卫或 schema |
| 巨型全局类型 | 任一变化影响所有模块 | 按领域拥有类型,在边界映射 |
| 可选字段表达状态 | 合法组合不清晰 | 判别联合 |
enum 与后端漂移 |
新值导致错误分支 | 运行时验证和未知值策略 |
验收时运行严格类型检查、单元测试和契约样本;再搜索本次边界中新增的 any、非空断言和双重断言。回滚类型定义不能修复已经写入的数据,因此涉及序列化格式时要先保持新旧读写兼容,再分阶段删除旧分支。
一手资料
资料与版本边界复核日期:2026-08-22。工具行为可能演进,实施前以锁定版本对应的官方文档为准。
评论