很多团队把 TypeScript 当成”加了类型注解的 JavaScript”——说白了就是给变量标个类型、把报错关小一点,真正跑起来还是一堆 any、一堆字符串常量、一堆”我知道这里可能有问题,但懒得处理就塞个断言”。结果 TS 逐渐退化成 AnyScript,类型系统的护城河形同虚设。都说 TypeScript 能”把 Bug 挡在编译之前”,可真要让它替你打工,靠的不是会写类型,而是懂得用类型去 表达约束。这篇文章想聊聊我是怎么从”会 TS”到”让 TS 替我消灭一整类 Bug”的。

类型不是给孩子贴的标签,是写给人看的契约
我见过最多的误用,是把类型当成”可选项”。一个接口传回来一堆字段,懒得全写,直接 interface X { data?: any };一个函数处理不同入参,图省事,参数写成 unknown 再断言。这样的类型只增加噪音,不增加信息,Bug 该从哪里冒出来还是从哪里冒出来。
真正的区别在于观念:类型是一份和编译器共同维护的契约。当你把入参、状态、响应都一一写清楚,编译器就不只是”翻译器”,而是一个不知疲倦的、在每次保存时都帮你跑一遍的验证器。举个最简单的例子——用可辨识联合(discriminated union)描述异步状态:init | loading | success | error 里,success 分支有 data、error 分支有 message,其余分支两者皆无。这么一写,想偷懒去 result.data 的地方,编译器会直接拦住你,逼你先处理 loading 和 error。从前要在运行时靠一堆 if (obj && obj.data) 兜住的边界,如今在编译期就被判了死刑。
同样的思路用在配置、路由、表单校验上屡试不爽。你在类型里多加的那几行字,其实是写给六个月后那个已经忘了上下文的自己,也写给未来接手这份代码的同事。

“编译期不可表示”:让非法状态干脆写不出来
类型系统的终极乐趣,在于把一些 Bug 从”靠人小心”变成”根本写不出来”。这里有两个我非常依赖的武器:字符串字面量类型 + 模板字面量类型,以及 branded/brand 类型。
先说模板字面量类型。做前端的人都知道”时间字符串”有多容易翻车——同一个字段在接口里可能是 "2026-09-08"、"2026/09/08" 或 timestamp。你要是定义一个 type ISODate = string,等于没说;但如果你用模板字面量去描述 `${number}-${number}-${number}`,再配合判别,至少把”格式必须一致”这条规律写进了类型签名里。路由、状态码、颜色值、事件名,凡是取值空间有限的东西,都值得用字面量类型封一层,而不是让每个调用方各自手写一份魔法字符串。
再说 brand 类型。真实业务里常有”ID 撞车”的隐患——订单 ID 和用户 ID 都是字符串,一个不小心传错,程序未必报错,却在夜深人静时给你埋一颗雷。通过形如 type OrderId = string & { readonly _brand: 'OrderId' } 的做法,给本就相同的底层值贴上不可伪造的”身份标签”,编译器就能在错误参数传入的瞬间给你红波浪线。诚然 brand 是运行时无差别的”假类型”,但它用极小的成本,把一整类”传错参数”的 Bug 拦在了 CI 之前,我认为非常划算。
渐进式引入:从 5% 到 80% 的落地方案
讲到这里,可能有朋友说:”道理我都懂,可我们的老项目几万行都是 JS,动不了。”我的建议是:别想着一步到位,从最容易咬合的地方开始。
第一步,先别急着把每个文件翻成 TS,而是把项目里最常出 Bug 的”公共边界”类型化——后端响应、表单数据、路由参数、枚举常量。这几处是不同模块互相”猜”的地方,也是最容易因为参数错配产生线上故障的地方。给它们配上严格的类型,性价比最高。
第二步,打开 strict 模式,把 noImplicitAny、strictNullChecks 全部拉满,并狠下心来用 eslint 规则禁止新增 any。有人会觉得这样开发变慢,其实恰恰相反:把一小撮 Bug 的排查时间从”运行时的半小时”前移到”编译期的三秒钟”,长期看是巨大的加速。
第三步,写公共的 Util 类型(Pick、Omit、Partial、Exclude 及其组合)做积木,让类型像搭乐高一样可复用。等到某天你发现自己”写类型”的功夫,已经从机械标注升级成”在设计 API 的形状”,说明你已经真正上手了。

结语:把能让机器做的事,放心交给机器
程序员这个职业,越往后越懂一件事:能把判断力留给真正需要创意和取舍的地方,是一种能力。运行时 Bug、参数错配、状态漏判这类机械性的疏漏,本就该交给类型系统在编译期去兜底,而不是靠某个人某天状态好时”多检查一眼”。
TypeScript 的回报从来不是线性的——当你越认真地把它当回事,它替你挡下的 Bug 就越多,你也越敢在重构时大胆地删删改改,因为编译器会在第一时间告诉你哪里碰线了。那种”改完一个类型,全工程几十处错误一起亮红”的瞬间虽然吓人,却恰恰证明:护城河是真的在起作用。别再把 TS 用成 AnyScript 了——给它一点信任,它会还你一份踏实。