给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了

69 阅读7分钟

三个月前,我只是想少写几个 console.log(typeof x)。三个月后,全组的 PR 通过率从 85% 掉到了 40%。


一、起因:一个 undefined 引发的血案

去年 Q4,我在改一个表单提交功能。接口返回的数据结构大概长这样:

TypeScript

// 后端实际返回的
{
  userInfo: {
    name: "张三",
    // 有时候有,有时候没有
    // 取决于用户有没有填
  }
}

我前端代码里直接 userInfo.phone.slice(0, 3),结果线上报了一个 Cannot read properties of undefined。排查了半小时,发现是某个老用户没填手机号。

这种 bug 在我这个项目里不是第一次了。祖传代码,Vue2 + JavaScript,前后换了 5 拨人,变量命名风格从驼峰到下划线到拼音全都有。每次改需求都像在拆盲盒——你永远不知道哪个属性是 undefined,哪个函数返回值是 Promise 但你忘了 await

那天晚上我失眠了。不是因为 bug,是因为我意识到:这种代码我改不动了。

二、决策:偷偷加 TS,只改我负责的模块

我跟组长提过两次全量迁移 TypeScript,都被否了。理由很充分:工期紧、风险大、其他同事不会 TS。

所以我决定:不请示,直接干。但只干我自己的一亩三分地。

我选了一个相对独立的业务模块——用户中心。这个模块代码量不大,但逻辑复杂,接口多,正好适合验证 TS 的价值。

第一步:渐进式引入

没有全量改写,而是 tsconfig.json 里开了 allowJs: true,让 JS 和 TS 共存。新文件写 .ts,老文件不动。

JSON

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "ESNext",
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "noImplicitAny": false
  }
}

注意我关了 strictnoImplicitAny。这是故意的——先让 TS 跑起来,别一上来就逼自己写一堆 any

第二步:从 API 层开始

我先把所有接口响应的类型定义补上了。这一步最枯燥,但收益最大。

TypeScript

// types/api.ts
interface UserInfo {
  name: string;
  phone?: string;  // 可选,因为可能没填
  email?: string;
  avatar?: string;
  createdAt: string;
}

interface ApiResponse<T> {
  code: number;
  data: T;
  message?: string;
}

然后给 axios 的响应加泛型:

TypeScript

// utils/request.ts
import axios from 'axios';

const request = axios.create({
  baseURL: '/api',
  timeout: 10000,
});

// 关键:给响应加类型
export function getUserInfo(): Promise<ApiResponse<UserInfo>> {
  return request.get('/user/info');
}

第三步:顺手加了 strict 模式

跑了两天,发现 strict: false 的情况下,TS 根本拦不住多少问题。比如我还是能写 const a = null; a.toString() 而不报错。

咬咬牙,开了 strict: true。瞬间报了 200 多个错误。我花了两个周末逐个修复,大部分是把 let 改成 const,补了一些 ?. 可选链,还有几个真正的逻辑漏洞。

三、意外:同事开始来"抄作业"了

我把这个模块的代码提交后,组长 review 的时候愣了一下。他说:"你这代码怎么突然变长了?"

我解释了一下 TS 的好处。他没说什么,merge 了。

一周后,隔壁组的同事私信我:"你那个 UserInfo 的类型定义能给我用一下吗?我们也在接用户中心的接口。"

两周后,另一个同事问我:"你那个 request.ts 里的泛型怎么写的?我也想给接口加类型。"

一个月后,组长在组会上说:"以后新模块统一用 TypeScript 吧,老模块逐步迁移。"

我当时的内心:???我只是想自己爽一下啊。

四、后果:代码审查标准被抬高了

事情从这里开始失控。

第一个月:类型定义成了标配

以前代码审查,大家关注的是"功能对不对""有没有内存泄漏"。现在审查的第一句话变成了:"这个参数的类型呢?"

TypeScript

// 以前这样写,完全没问题
function formatDate(date) {
  return dayjs(date).format('YYYY-MM-DD');
}

// 现在必须这样
function formatDate(date: string | Date | number): string {
  return dayjs(date).format('YYYY-MM-DD');
}

有同事抱怨:"我就写个工具函数,还要写类型?以前 5 分钟搞定,现在 15 分钟。"

第二个月:any 成了审查红线

有个老同事习惯了 JS 的灵活,写了个:

TypeScript

const data: any = await fetchSomething();
data.something.map(...);

审查直接被打了回来:"不要用 any,用 unknown 然后做类型守卫。"

老同事私下跟我说:"你们这些写 TS 的,是不是见不得别人用 any?"

我说:"不是见不得,是 any 会让 TS 变成 JS,那我们加 TS 的意义就没了。"

他回了我一个微笑表情,但我感觉他内心在骂我。

第三个月:全组 PR 通过率暴跌

这是最离谱的。

以前组里 PR 通过率大概 85%,大部分是一次过,小修小改。现在掉到了 40%。不是代码质量变差了,是审查标准变严格了

以前审查者的心态:"能跑就行,别出线上问题。"

现在的心态:"这个类型推导是不是有问题?这个联合类型能不能更精确?这个泛型参数能不能简化?"

有个实习生提交了一段代码,审查来回改了 7 次。最后实习生哭了,说:"我只是想加个按钮,为什么比写毕业论文还难?"

五、反思:TS 到底带来了什么?

好处,是真实的:

  1. 线上 bug 少了。以前每个月 3-5 个 undefined 相关的报错,现在基本归零。
  2. 重构胆子大了。以前改个函数签名,要全局搜索所有调用处。现在改完,编译器直接告诉你哪里错了。
  3. 新人上手快了。接口文档和类型定义合二为一,新同事看代码不用猜字段了。

代价,也是真实的:

  1. 开发速度下降了。前期写类型、修类型错误的时间,大概占了总工时的 20-30%。
  2. 团队学习成本。3 个同事去买了 TS 课程,2 个老同事到现在还在用 as any 绕过检查。
  3. 审查摩擦增加了。以前审查是"找 bug",现在是"辩论类型设计",有时候为了一个 interfacetype 的选择能吵半天。

六、给想引入 TS 的团队的忠告

1. 不要偷偷加,要提前沟通

我这次能成,纯属运气好——组长正好也想推技术升级。如果组长是个保守派,我可能就是"那个擅自改技术栈的刺头"了。

2. 渐进式迁移,不要一刀切

allowJs: true 是救命稻草。让 JS 和 TS 共存,老代码不动,新代码用 TS,这是最现实的方案。

3. strict 模式可以晚点开

先跑起来,再严格。一上来就 strict: true,团队会被报错淹没,直接劝退。

4. 类型定义是公共资产

我把 types/api.ts 抽成了一个独立的包,全公司共享。这样大家不用重复定义,也保证了接口类型的一致性。

5. 接受"不完美"的 TS

有些老代码,类型就是写不清楚,那就 // @ts-ignore 或者 as。不要为了追求 100% 的类型覆盖,把代码改成不可读的样子。

七、最后

三个月后的今天,全组 12 个人,有 10 个已经习惯了 TS。剩下 2 个老同事还在坚持用 JS,但他们的代码审查通过率确实比别人低。

上周 CTO 在全员大会上说:"我们前端团队的代码质量最近提升很明显,代码审查的深度和严谨度值得其他组学习。"

组长在会上看了我一眼,眼神复杂。

我知道他想说什么: "是你小子把审查标准抬高的吧?"

总结:我给祖传 JS 项目偷偷加了 TypeScript,本想自己爽,结果全组审查标准被拉高,PR 通过率从 85% 暴跌到 40%,CTO 却在大会上表扬了我们代码质量。

有纸飞机问题,👆👆👆分享给大家,不用客气,可做参考!

你们团队引入 TS 的时候,有没有类似的"阵痛"?或者你们还在用 JS,觉得 TS 是过度工程?评论区聊聊。