← Back to Blog

别再把 TypeScript 用成 “AnyScript”:让类型系统替你扛走一整类 Bug

26 阅读 点赞

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

深蓝色调下流动发光的TypeScript代码矩阵,未来科技感
把类型当成与编译器共同维护的契约,才能让 TypeScript 真正替你兜底

类型不是给孩子贴的标签,是写给人看的契约

我见过最多的误用,是把类型当成”可选项”。一个接口传回来一堆字段,懒得全写,直接 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 拦在编译之前

“编译期不可表示”:让非法状态干脆写不出来

类型系统的终极乐趣,在于把一些 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 模式,把 noImplicitAnystrictNullChecks 全部拉满,并狠下心来用 eslint 规则禁止新增 any。有人会觉得这样开发变慢,其实恰恰相反:把一小撮 Bug 的排查时间从”运行时的半小时”前移到”编译期的三秒钟”,长期看是巨大的加速。

第三步,写公共的 Util 类型(PickOmitPartialExclude 及其组合)做积木,让类型像搭乐高一样可复用。等到某天你发现自己”写类型”的功夫,已经从机械标注升级成”在设计 API 的形状”,说明你已经真正上手了。

杂乱的噪点碎片逐渐凝聚成一条清晰的发光线,象征代码从混乱走向规范
当类型成为习惯,机械性的疏漏自然会从代码里一点点消失

结语:把能让机器做的事,放心交给机器

程序员这个职业,越往后越懂一件事:能把判断力留给真正需要创意和取舍的地方,是一种能力。运行时 Bug、参数错配、状态漏判这类机械性的疏漏,本就该交给类型系统在编译期去兜底,而不是靠某个人某天状态好时”多检查一眼”。

TypeScript 的回报从来不是线性的——当你越认真地把它当回事,它替你挡下的 Bug 就越多,你也越敢在重构时大胆地删删改改,因为编译器会在第一时间告诉你哪里碰线了。那种”改完一个类型,全工程几十处错误一起亮红”的瞬间虽然吓人,却恰恰证明:护城河是真的在起作用。别再把 TS 用成 AnyScript 了——给它一点信任,它会还你一份踏实。

💬 发表评论