TypeScript 最有用的地方,通常是先把运行时的不确定性收窄,再让后面的代码保持简单。
从 API 取回的数据应该先被看成 unknown,而不是直接断言成某个接口。一个小的类型守卫可以检查对象、字段和数组,让错误停在边界上。断言 as User 只是在告诉编译器“相信我”,不会替你检查服务端真的返回了什么。
联合类型适合表达有限状态。例如请求状态可以是 idle、loading、success 或 error,每种状态携带自己的数据。用 switch 处理时开启穷尽检查,新增状态以后编译器会提醒所有需要修改的地方。比起多个可选字段,这种写法更难出现“loading 却有旧 error”的矛盾组合。
字面量类型和 as const 能把配置对象的值保留下来,但不要把每个对象都冻结成一串窄类型。类型的目标是帮助表达约束,不是让推导结果变得无法阅读。遇到复杂类型时,先给它一个有意义的别名,说明它解决了什么问题。
泛型真正适合出现的地方,是函数需要保持输入和输出的对应关系,例如从数组里取出同类型元素、给分页响应保留数据类型。若泛型只是把 any 换成一堆尖括号,却没有提供更强的关系,删掉它反而更诚实。
类型收窄不能代替运行时校验。表单输入、URL 参数、后端响应和本地存储都来自程序边界,那里没有 TypeScript 的保护。把校验集中在边界层,进入业务逻辑以后使用已经验证过的类型,代码会更容易读和测。
我现在判断一个类型设计是否值得留下,只看它有没有减少分支、让错误更早出现,或者让调用者不必猜测。类型不是装饰,也不是竞赛题;它应该安静地把不可能的状态挡在门外。
类型设计也要接受删除。一个类型别名如果只在一处出现,却让错误信息变得很长,可能还不如一个清楚的对象结构。每次重构以后跑一遍真实边界测试,确保类型安全没有停在编辑器里。
类型收窄最终要落到边界测试上。除了成功响应,还要准备缺字段、字段类型错误、未知状态和空数组;这些输入没有 TypeScript 类型声明可以替我们挡住,只能由运行时校验处理。
类型越复杂,越应该问它是否真的减少了调用者的判断。如果一个泛型让错误信息变长,却没有表达新的关系,那它只是把不确定性藏到了更深的地方。能读懂、能测试和能删除,都是类型设计的一部分。
在实际代码里,我更倾向于把解析函数的输入写成 unknown,先判断是否为对象,再逐字段收窄。校验失败时返回带路径的错误,例如 user.profile.name 缺失,而不是让业务层接收一堆可能为空的字段。
测试也要覆盖未知状态、错误类型和旧版本数据。类型检查只能覆盖编译时路径,运行时输入仍然需要样本。边界测试通过以后,类型别名才有资格留在公共接口里。