从 JavaScript 到 TypeScript 的平滑迁移:大型前端项目的实战经验总结

为什么我们要在大型项目中迁移到 TypeScript

三年前,我所在团队维护着一个超过 30 万行代码的前端项目。它诞生于 JavaScript 的黄金时代,模块化靠 Webpack,类型靠注释和祈祷。随着业务膨胀,代码的可维护性急剧下降:改一个函数,不知道有多少调用方会受影响;重构一个模块,测试覆盖率再高也挡不住运行时才发现的 undefined is not a function。我们决定迁移到 TypeScript,不是因为它流行,而是因为我们需要一种能在编译期就暴露问题的工具。

但“迁移”两个字听起来简单,做起来却是一场持久战。下面是我们踩过的坑和总结出的经验,希望能帮到正在犹豫或已经上路的你。

第一步:不要试图一夜换血,先让 TS 和 JS 共存

很多团队失败的原因,是一上来就把所有 .js 文件重命名为 .ts,然后打开 strict 模式,结果几千个错误铺天盖地,士气瞬间归零。我们的做法是:

  • 保持现有 JavaScript 代码不动,新增文件一律用 TypeScript 编写。
  • tsconfig.json 中设置 "allowJs": true,让 TS 编译器能处理 JS 文件。
  • 逐步将旧文件重命名为 .ts,每次只处理一个模块。

这个阶段的目标不是“全部类型化”,而是“让类型系统开始工作”。我们用了大约两个月,才把核心工具库和公共组件迁移完毕。

第二步:配置合理的编译选项,避免过早严格

TypeScript 的 strict 模式是终极目标,但绝不是起点。我们最初的 tsconfig.json 长这样:

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "ESNext",
    "allowJs": true,
    "checkJs": false,
    "noImplicitAny": false,
    "strictNullChecks": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  }
}

注意 noImplicitAnystrictNullChecks 都是关闭的。这让我们在迁移初期不会被大量 anynull 检查淹没。每完成一个模块的迁移,我们就在该模块的目录下加一个 .eslintrctsconfig 覆盖,逐步打开严格选项。

第三步:用 JSDoc 类型注释作为过渡桥梁

对于暂时不想重命名的 .js 文件,我们大量使用 JSDoc 来提供类型信息。TypeScript 能识别 JSDoc 中的 @type@param@returns,这让我们在不改文件扩展名的情况下获得类型检查。

例如:

/**
 * @param {string} userId
 * @param {number} [timeout=3000]
 * @returns {Promise<User>}
 */
async function fetchUser(userId, timeout = 3000) {
  // ...
}

这样,调用方在 TS 文件中就能获得参数提示和返回值类型。JSDoc 成了我们迁移路上的“脚手架”,等模块稳定后再整体转为 .ts 文件。

第四步:处理第三方库和全局类型

大型项目必然依赖大量 npm 包。有些包自带类型,有些需要 @types/xxx,还有些完全没有类型。我们的策略是:

  1. 优先寻找社区维护的 @types 包。
  2. 如果没有,就在 src/types 下写一个 declarations.d.ts,用 declare module 'xxx' 声明为 any
  3. 对于内部私有包,推动维护者补充类型,或者我们自己在 types 目录下写最小可用声明。

不要小看 declare module 的威力,它能让编译器闭嘴,让你先跑起来。等业务稳定了,再回头补精确类型。

第五步:建立类型检查的 CI 关卡

迁移过程中最怕的是“今天修好了,明天又退回去”。我们在 CI 中增加了两个步骤:

  • tsc --noEmit:确保没有类型错误。初期可以只检查新增的 TS 文件,用 --project 指定子目录。
  • eslint --ext .ts,.tsx:配合 @typescript-eslint 插件,禁止显式 any(除非有注释豁免)。

一旦 CI 通过,就合并。任何新代码必须通过类型检查,旧代码可以暂时豁免,但豁免列表只能缩小,不能扩大。

第六步:团队共识与代码审查

技术迁移本质上是人的迁移。我们做了几件事:

  • 每周一次“TypeScript 诊所”,集中解决疑难类型问题。
  • 在代码审查中,对新增的 any 要求必须写注释说明原因。
  • 鼓励用 unknown 代替 any,用类型守卫代替类型断言。
  • 分享会:让已经迁移完的同事演示如何用泛型、联合类型重构旧代码。

三个月后,团队里再也没有人想回到纯 JavaScript 了。

实战中的三个典型陷阱

陷阱一:过度使用 any。迁移初期为了快速通过编译,很多人随手写 any。结果是类型系统形同虚设。我们的对策是:any 必须配注释,且 CI 统计 any 数量,只允许下降。

陷阱二:忽略 strictNullChecks 的长期成本。我们关闭它太久,导致后期打开时出现了上千个错误。建议在迁移完成 60% 左右时,就分模块打开 strictNullChecks,不要拖到最后。

陷阱三:类型定义与运行时脱节。有人写了漂亮的接口,但实际数据来自后端,字段可能缺失。我们后来引入了 zod 做运行时校验,再用 z.infer 推导类型,保证类型和实际数据一致。

迁移后的收益与反思

迁移完成一年后,我们统计了几个关键指标:

  • 生产环境类型相关错误下降 92%。
  • 新成员上手时间从平均 3 周缩短到 1.5 周。
  • 重构信心大幅提升,以前不敢动的核心模块现在可以放心修改。

但最大的收获不是这些数字,而是团队对“类型即文档”的认同。TypeScript 不是银弹,它不能替代测试,也不能自动修复糟糕的设计。但它能在你犯错时,第一时间告诉你。对于大型前端项目,这种即时反馈是无价的。

如果你正准备迁移,记住:慢就是快,共存优于替换,工具服务于人。祝你的迁移之路平滑顺利。

未经允许不得转载:任鹏个人博客 » 从 JavaScript 到 TypeScript 的平滑迁移:大型前端项目的实战经验总结

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏