重构第三天,我把项目里 500 个 any 全部换成了具体的 Interface,然后项目崩了😭
![]()
开始在重构旧项目的最近一个月,我每天打开项目代码,心情都像是在上坟😖。
这个项目是5年前的老代码,说是用了 TypeScript,但含 any 量高达 80%。
User 是 any,Response 是 any,连 window 也是 (window as any)。
每次写业务,我都得猜这个 res.data.list 到底是个数组,还是个 null,还是后端心情不好传回来的一个空字符串。
作为一个有代码洁癖的资深前端,我忍不了😡。
每周五下午,趁着业务需求的空窗期,决定搞个大扫除。
目标是:干掉核心模块里的所有 any,还 TypeScript 一个明确的类型。
过程是 - 爽是爽了
那个周末我是在多巴胺的海洋里度过的。
我对着接口文档,把那些面目可憎的 any 一个个替换成了极其优雅的 Interface。
Refactor 前:
// 屎山代码
function renderUser(user: any) {
const name = user.info.name; // 这里的 info 可能是 undefined,但 TS 不报错
const age = user.age.toFixed(2); // 这里的 age 可能是 string,TS 也不报错
return `User: ${name}, Age: ${age}`;
}
Refactor 后:
// 优雅代码
interface UserInfo {
name: string;
avatar?: string;
}
interface User {
id: number;
info: UserInfo; // 必须存在
age: number; // 必须是数字
}
function renderUser(user: User) {
// 此时 IDE 智能提示全出来了,简直丝滑
return `User: ${user.info.name}, Age: ${user.age.toFixed(2)}`;
}
看着 VS Code 里那一个个红色的波浪线被我修好,看着 TS 编译通过的绿色对勾,我感觉自己就是代码界的上帝🥱。我甚至顺手把以前代码里那些丑陋的像 if (user && user.info) 防御性判断给删了—— 因为 Interface 定义里写了,info 是必选属性,不可能为空!
周一早上,我信心满满地提了 Merge Request,顺便把代码推上了测试环境。
我心想:兄弟们,以后你们调接口都有智能提示了😁。
事故发生了:P1 级白屏,我在群里被 @ 成了筛子
上午 10 点的时候,测试环境崩了。
![]()
![]()
上午 10 点半,技术总监冲到我工位:你动核心模块了?怎么列表页全白了?控制台全是报错!
我一愣:不可能啊,我本地编译全通过了,TS 类型检查也是完美的。
打开控制台一看,我傻眼了。满屏红字:
Uncaught TypeError: Cannot read properties of undefined (reading 'name')Uncaught TypeError: user.age.toFixed is not a function
![]()
怎么会?
我明明定义了 interface User,里面写死了 info 必须存在,age 必须是 number 啊!
真相是,原来TypeScript 是最大的骗子😡
经过两个小时的狼狈排查,我终于明白了那个让所有 TS 新手都会摔坑的真理:
TypeScript 的类型,在运行时(Runtime)就是个屁!
后端的嘴,骗人的鬼
Interface 定义里,我信了后端的文档,写了 age: number。
但实际上,老数据里有几百条数据,age 存的是字符串 "18"😡。
以前用 any 的时候,JS 隐式转换还能跑(或者之前压根没调 toFixed)。
现在我为了规范,加了 .toFixed(2),因为 TS 告诉我它是数字。
结果运行时,浏览器拿着字符串 "18" 去调 toFixed,直接炸穿😥。
必选属性莫名其妙的消失
Interface 里我写了 info: UserInfo(必选)。
于是我自信地删掉了 if (user.info) 的判空逻辑。
结果后端返回的数据里,因为历史脏数据,真的有几个用户的 info 是 null😡。
TS 编译时很开心,浏览器运行时直接抛出 Cannot read properties of null。
我把编译时的类型安全,误当作了运行时的数据安全。
我以为我在写 Java,其实我还在写 JavaScript。TS 编译成 JS 后,那些漂亮的 Interface 全都被擦除得干干净净,裸奔的数据依然是那个烂样子。
反思反思反思:这 500 个 any,原来是护身符😥
看着回滚后的代码,那个丑陋的 any 重新回到了屏幕上,我竟然感到了一丝安全感。
这次事故给了我三个血淋淋的教训:
别太迷信文档,要信数据。
后端的 Swagger 文档写写而已,你真信了 Required,上线就得背锅。
TS 是协定,Zod 才是严格的。
如果你真的想保证数据类型安全,光写 Interface 没用(那只是给 IDE 看的)。你得上运行时校验库(比如 Zod 或 Runtypes)。
// 这才是真安全
const UserSchema = z.object({
age: z.number(), // 运行时如果拿到 string,这里直接抛错,而不是等到业务逻辑里炸开
});
防御性编程永远不要删 !!!
不管 TS 告诉你这个字段多一定存在,只要数据源来自网络(API),老老实实写 Optional Chaining (user?.info?.name)。
最后的结局
那天下午,我默默地把自己写的那些 interface 改成了原样:
interface User {
// 认怂了,全部改成可选 + 联合类型😒
age?: number | string;
info?: UserInfo | null;
}
虽然代码里又要加回那些烦人的 if 判断,虽然类型提示没那么完美了,但至少——它不崩了😒。
如果你也想重构项目里的 any,听哥一句劝:
除非你上了 Zod 做运行时校验,否则那个丑陋的 any,可能是你项目里最后一道防线。
分享完毕🙌
![]()