Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/TypeScript 深度教程/第18章

第18章:设计精华 —— 写出可靠 TypeScript 的原则

8 分钟 · 更新于 2026-09-01

第18章:设计精华 —— 写出可靠 TypeScript 的原则

收尾章。把前十七章压缩成一套工程判断:让 JavaScript 运行时事实与 TypeScript 静态模型保持一致;让外部数据先验证、内部状态可穷尽、公共 API 可理解;让类型系统降低风险,而不是隐藏风险。


一、统一命题:类型必须对应事实

TypeScript 最大的风险不是“类型不够高级”,而是类型声明与运行时事实不一致。

场景错误做法可靠做法
API 响应json as Userunknown → 运行时校验 → User
状态管理多个独立布尔值判别联合
找不到数据非空断言 !显式处理 undefined
第三方 JS整包声明为 any按真实 API 补 .d.ts
通用函数输入输出都是 any泛型保留关系
模块别名只配 paths编译器、bundler、运行时一致配置
错误处理假设 catch 是 Errorunknown 收窄
更新模型Partial<Entity>按业务规则定义命令

二、TypeScript 的安全边界

TypeScript 可以在编译期证明一部分事实:

  • 属性和参数是否类型兼容。
  • 联合分支是否完整。
  • 泛型关系是否保持。
  • 模块和声明是否可解析。

它不能证明:

  • 网络数据真实符合接口。
  • 权限、金额和状态转换符合业务规则。
  • 异步任务一定按期完成。
  • 数据库与缓存一致。
  • 程序没有性能、安全和逻辑问题。

可靠系统需要:

text
静态类型 + 运行时验证 + 行为测试 + 监控与错误策略

三、十一条核心原则

原则 1:先学 JavaScript,再理解 TypeScript

类型被擦除后,闭包、this、Promise、原型、事件循环仍按 JavaScript 运行。

原则 2:新项目默认严格

开启 strict,再按项目风险评估 noUncheckedIndexedAccessexactOptionalPropertyTypes 等选项。关闭规则必须有明确原因。

原则 3:让推断处理局部,让标注表达边界

局部变量避免重复标注;函数参数、公共返回值、模块 API 和外部协议应清晰定义。

原则 4:不确定值使用 unknown

unknown 迫使调用者验证,any 则让错误扩散。把 any 视为有期限的迁移债务。

原则 5:类型断言不是校验

每个 as 都应回答:“我掌握了什么编译器不知道的事实?”如果答案只是“想消除报错”,应修复模型或增加运行时检查。

原则 6:用联合表达有限状态

判别联合比多个可选字段和布尔值更可靠,可与 switchnever 配合穷尽检查。

原则 7:泛型用于保存关系

若类型参数只出现一次,通常没有建立关系。泛型不是“更高级的 any”,也不是越多越好。

原则 8:从稳定源派生类型

使用 keyof、索引访问、映射和工具类型减少重复;更进一步,可从运行时 schema 或协议定义生成静态类型。

原则 9:模块配置服从运行时

tsconfigpackage.json、Node 和 bundler 必须讲同一种模块语言。类型检查通过不代表运行时一定能加载。

原则 10:公共类型首先服务使用者

错误信息、补全体验和文档可读性比类型技巧更重要。复杂类型应封装并命名,不把实现细节暴露给调用者。

原则 11:类型系统也需要测试和性能预算

为工具类型编写正确/错误用例;为声明文件做消费测试;为复杂类型观察编译时间和编辑器响应。

原则 12:模块配置必须模拟真实宿主

先确定 Node、浏览器还是 bundler 负责加载代码,再选择 modulemoduleResolution、扩展名和 package.json 策略。TypeScript 不是模块加载器。

原则 13:声明文件是运行时事实的合同

.d.ts、模块增强和全局声明必须与真实 JavaScript 一致,并通过 ESM/CJS 或 bundler 消费测试验证。

原则 14:JavaScript 可以渐进类型化

allowJscheckJs 和 JSDoc 能在不立即改写全部源码的情况下建立反馈。迁移目标是缩小不可信边界,不是追求扩展名统一。

原则 15:浏览器资源也要设计生命周期

DOM 查询要处理可空与元素类型;事件监听器、计时器、Observer 和连接要明确清理;JSX 类型规则以具体框架声明为准。

原则 16:构建工具各司其职

tsc、Babel/SWC/esbuild、bundler 和测试工具解决不同问题。转译成功不等于类型检查成功,降低 target 也不等于提供运行时 polyfill。

原则 17:结构兼容不等于绝对安全

TypeScript 为 JavaScript 生态接受部分不健全规则。公共库、可变容器和回调边界要从真实调用方向审查,而不能只以“编译通过”为依据。

原则 18:版本升级属于工程迁移

Release Notes 应转化为配置、类型推断、模块行为、声明兼容和性能的检查清单。升级编译器后必须运行完整反馈链,不能用批量断言掩盖变化。

四、any 治理

可接受的临时场景:

  • 大型 JS 项目迁移的局部边界。
  • 第三方声明暂时缺失。
  • 类型系统当前无法表达、且已有运行时保证的极小范围。

治理方式:

  1. any 限制在边界模块。
  2. 立即转为 unknown 或领域类型。
  3. 添加原因、负责人和清理条件。
  4. CI 统计新增 any 或抑制指令。
  5. 不让 any 进入公共 API。

五、断言治理

风险从低到高大致为:

text
经过运行时检查后的局部断言
< DOM/框架不变量断言
< 非空断言
< 普通 as
< 双重断言 as unknown as T

优先替代方案:

  • 类型守卫。
  • 判别联合。
  • 更准确的泛型约束。
  • satisfies
  • schema 校验。
  • 调整数据流让不变量可见。

六、API 设计

可靠 API 应回答:

  • 输入是否可能为空或失败?
  • 函数是否修改参数?
  • 返回 Promise 是否支持取消?
  • 错误是异常、Result 还是状态码?
  • 泛型关系是否能由调用点推断?
  • 类型是否泄露内部实现或第三方依赖?
  • 命名是否表达同步/异步、创建/查询和副作用?

示例:

ts
function findUser(id: UserId): User | undefined;
function requireUser(id: UserId): User;
async function loadUser(id: UserId, signal?: AbortSignal): Promise<User>;

名字和返回类型共同表达不同失败语义。

七、领域建模

不要直接用原始字符串和数字承载所有概念:

ts
type UserId = Brand<string, "UserId">;
type AmountInCents = Brand<number, "AmountInCents">;

不要用一份巨型接口贯穿数据库、服务端、网络和 UI:

text
数据库模型 → 领域模型 → DTO → 视图模型

各层只包含所需字段,通过明确转换保持边界。

八、错误处理

建立错误分类:

  • 验证错误:告诉调用者字段和原因。
  • 业务拒绝:稳定错误码,可预期处理。
  • 依赖失败:可重试、降级或上报。
  • 程序错误:不变量破坏,需要修复。

不要把所有错误都变成字符串,也不要把所有异常都吞成 undefined

九、异步可靠性

类型为 Promise<T> 只表示未来结果,不包含:

  • 超时。
  • 取消。
  • 重试。
  • 幂等。
  • 并发上限。
  • 顺序保证。
  • 资源清理。

这些都要进入 API 和运行协议。AbortSignal、队列容量、finally 清理和错误分类比“返回类型写对”更重要。

十、性能原则

编译性能

  • 避免深递归条件类型。
  • 减少巨大联合和重复实例化。
  • 用 Project References 切分项目。
  • 使用诊断工具定位,而非关闭严格模式。

运行性能

  • 类型大多被擦除,不要凭类型复杂度猜运行速度。
  • 关注真实输出代码、bundle、渲染、I/O 和算法。
  • 运行时校验有成本,但应与边界风险比较。
  • 先测量再优化。

十一、常见反模式

  1. JSON.parse(...) as T
  2. 到处使用非空断言 !
  3. 公共函数接受或返回 any
  4. 多个布尔字段表达互斥状态。
  5. 所有更新接口都用 Partial<T>
  6. 对象类型全部写成宽泛 Record<string, any>
  7. 高级类型一行数百字符且无命名。
  8. 把类型错误当障碍,用断言绕开。
  9. 只依赖 IDE,不在 CI 运行 tsc
  10. 构建工具能出包就认为类型检查完成。
  11. 模块别名只配置一处。
  12. 发布库不测试生成的声明和消费方式。
  13. 类型与运行时 schema 各写一份并长期漂移。
  14. 把 JavaScript 行为误认为 TypeScript 会修正。
  15. 用框架类型污染领域核心。
  16. Node、bundler 和浏览器项目复制同一份模块配置。
  17. Babel 或 bundler 能转译就移除 tsc 检查。
  18. 发布声明文件但不测试 ESM/CJS 消费。
  19. DOM 查询和资源监听大量依赖非空断言。
  20. 用结构兼容性掩盖本应区分的领域身份。

十二、代码审查清单

类型与模型

  • 是否存在可用判别联合消除的矛盾状态?
  • 是否把外部数据直接断言为可信类型?
  • any! 是否有充分理由?
  • 公共 API 是否容易理解和推断?

运行时

  • 校验、错误、超时和取消是否明确?
  • 类、枚举、装饰器产生的运行时代码是否符合预期?
  • 日期、BigInt、Map 等跨 JSON 边界是否正确转换?

工程

  • 模块配置与运行环境是否一致?
  • 声明文件是否与实现同步?
  • 类型测试、行为测试和消费测试是否覆盖?
  • 类型复杂度是否影响编译性能?

十三、后续学习路线

前端应用

  • DOM 类型、React/Vue 类型模式、状态机、表单 schema、API Client。

Node.js 与后端

  • NodeNext 模块、流、AbortSignal、运行时校验、数据库类型边界、错误协议。

库与工具链

  • 声明文件、模块发布、SemVer、API Extractor、类型测试、代码生成。

类型系统深入

  • Handbook 的 Everyday Types、Narrowing、Functions、Objects、Creating Types from Types、Declaration Files。

十四、全书结语

学习 TypeScript 的典型过程是:

text
给变量加类型
→ 理解联合与收窄
→ 用泛型保存关系
→ 从已有定义派生类型
→ 设计运行时边界
→ 用类型表达领域状态和模块契约

真正掌握 TypeScript,不是能写出最长的条件类型,而是能回答:

  • 这个类型对应什么运行时事实?
  • 数据从哪里进入可信区域?
  • 哪些状态应被类型消除?
  • 这个 as 为什么成立?
  • 失败、为空、取消和超时是否被表达?
  • 公共 API 是否让调用者自然写出正确代码?
  • 模块、声明、构建和运行是否一致?
  • 当前配置模拟的宿主究竟是谁?
  • .d.ts 是否通过真实消费项目验证?
  • DOM、事件、异步任务和其他资源由谁清理?

可靠 TypeScript 的最终原则可以压缩成一句话:让静态模型忠实描述运行时事实,并在事实尚未建立的边界执行验证。

十五、总结

  1. TypeScript 是 JavaScript 的静态分析层,不是独立运行时。
  2. 类型安全的基础是严格模式、正确建模和减少 any/断言。
  3. 判别联合、泛型和派生类型用于表达状态与关系。
  4. 外部数据必须经过运行时验证。
  5. 模块配置、声明文件与真实运行环境必须一致。
  6. 类型测试、行为测试、消费测试和性能诊断共同构成工程闭环。
  7. 模块系统由真实宿主决定,声明文件必须与运行时导出一致。
  8. JavaScript、DOM、JSX 和多构建工具场景都需要明确边界与生命周期。
  9. 结构兼容存在有意的不健全取舍,编译通过不是绝对安全证明。
  10. 类型复杂度应服务使用者和维护者,而不是展示技巧。

原始资料引用


  • TypeScript 深度教程
  • 第4章:类型建模
  • 第6章:模块化工程
  • 第7章:高级类型
  • 第10章:综合实战
  • 第11章:兼容性与推断
  • 第12章:模块深潜
  • 第13章:声明文件
  • 第14章:JavaScript 渐进类型化
  • 第15章:浏览器开发
  • 第16章:构建与工具链
  • 第17章:版本演进

本章目录
一、统一命题:类型必须对应事实二、TypeScript 的安全边界三、十一条核心原则四、any 治理五、断言治理六、API 设计七、领域建模八、错误处理九、异步可靠性十、性能原则十一、常见反模式十二、代码审查清单十三、后续学习路线十四、全书结语十五、总结原始资料引用Related Documents
苏ICP备2025204887号-2