跳到主内容

用 AI 把 JavaScript 老项目迁到 TypeScript:分批类型化的策略与踩坑清单

100%
用 AI 把 JavaScript 老项目迁到 TypeScript:分批类型化的策略与踩坑清单

老项目迁 TypeScript 最稳的路线不是一次性全量改造,而是「先建基线、再分批收紧」:先用 allowJs 把项目跑起来,按依赖方向从底层模块开始分批开启 strict,每一批只交给 AI 有限范围的文件,并用 tsc --noEmit 的报错条数作为收敛指标。这样做的好处是迁移可以随时停在任意一批,仓库始终可发布。

为什么全量迁移几乎必失败

全量迁移的典型做法是:跑一遍转换脚本,把 .js 全改成 .ts,然后面对几千条报错。问题不在报错多,而在于三个后果同时出现:

  • 报错无法归因:类型错误互相掩盖,改一处可能”修好”另一处,实际只是把错误挪了位置。
  • 评审失效:一次几千行的 diff 没人能真正 review,AI 的改动也就没人兜底。
  • 无法回退:迁移分支和主分支长期分叉,合回去时业务代码已经变了。

分批方案的核心是把这三点拆掉:每批 diff 小到可以评审,报错数只降不升,随时能合并回主干。

第一步:建立可度量的类型基线

在让 AI 动手之前,先把”现状”固化下来,否则你无法判断迁移是在前进还是在原地打转。

1. 让 TypeScript 接管但不阻断

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "noEmit": true,
    "strict": false,
    "target": "ES2022",
    "moduleResolution": "bundler",
    "skipLibCheck": true
  },
  "include": ["src"]
}

allowJs 是关键:它让 .js 与 .ts 共存,迁移期间不需要一次性重命名。checkJs 先关掉,避免一开始就淹没在旧代码的类型错误里。

2. 补依赖类型

先跑一次 npm i -D typescript @types/node,再逐个补第三方库的 @types/*。缺类型声明的库先加一行 declare module "xxx"; 占位,记为技术债,不要在第一批就处理。

3. 记录基线数字

把 tsc --noEmit | wc -l 的报错条数、any 出现次数、.js 文件数写进一个 MIGRATION.md。这三个数字就是后面每批 PR 的验收标准。

第二步:分批顺序按依赖方向排,不按目录排

很多团队按目录顺序迁,结果底层工具函数还是 any,上层的类型推导全是 any,白做。正确顺序是被依赖最多的先迁:

批次范围开启选项验收标准
第 0 批常量、枚举、类型定义文件strict: false纯声明,零报错
第 1 批utils / helpers / 数据模型strict: false对外导出全部有显式类型
第 2 批service / repository 层strictNullChecks: true报错数比基线下降
第 3 批controller / 路由 / 组件strict: true该目录 any 归零
收尾全仓库strict: true + checkJs: false剩余 .js 有明确迁出计划

判断”被依赖最多”不用猜,一条命令即可:

# 按被 import 次数倒序列出文件,排在前面的就是第一批
rg -o "from ['\"]([^'\"]+)['\"]" -r '$1' src | sort | uniq -c | sort -rn | head -30

第三步:把哪些活交给 AI,哪些活留给人

AI 在迁移里最值钱的是”机械但费眼”的部分,而不是架构判断。实测下来分工大致是:

适合交给 AI 的四类改动(点击展开)
  1. 从运行时值反推类型:给它几段真实入参样本,让它写出接口定义,比人肉读代码快得多。
  2. 补 JSDoc 类型标注:对还没转成 .ts 的 .js 文件,先加 JSDoc 风格的 @param / @returns,配合 checkJs 就能提前享受类型检查。
  3. 批量收敛 any:让它把 any 换成 unknown 并补上必要的类型守卫,这一步人做极易漏。
  4. 生成迁移报告:每批结束后让它输出”改了哪些公共签名、是否有行为变更”,直接贴进 PR 描述。

反过来,这三件事不要交给 AI 自行决定:是否改变函数签名、是否引入运行时校验、是否为兼容旧调用方保留重载。

给 AI 的指令里一定要写死边界:”只修改 src/utils/ 下的 6 个文件,不得改动任何函数的入参顺序与返回结构,不得新增依赖,改完运行 npx tsc –noEmit 并把报错数从 X 降到 Y 。”可量化的边界比”帮我优化类型”有效十倍。

四个几乎必踩的坑

坑一:第三方库没有类型声明

不要急着写完整 .d.ts。先用最小声明顶住,把真正的类型工作推到专项批次:

// types/shims.d.ts
declare module "legacy-chart-lib" {
  const render: (el: HTMLElement, opts: Record<string, unknown>) => void;
  export default render;
}

坑二:this 的隐式 any

老代码里大量 function 声明 + call/apply,转 TS 后 this 全是隐式 any。对策是在开启 noImplicitThis 之前,先把这些函数逐个确认调用方式,能改箭头函数就改,改不了的加显式 this 参数。

坑三:动态属性访问

obj[dynamicKey] 在 strict 下直接报错。正确做法不是加 any,而是把键收窄成联合类型或用 Map:

type StatusKey = "draft" | "published" | "archived";
const labelMap: Record<StatusKey, string> = {
  draft: "草稿", published: "已发布", archived: "已归档",
};

坑四:构建配置没跟上

编译过了不代表打包能过。 Babel 项目要加 @babel/preset-typescript,Vite 项目确认 esbuild 能处理 .ts,Webpack 要检查 resolve.extensions 是否包含 .ts。这一步建议单独一个 PR,不混在类型批次里。

迁移期的日常怎么排

最实用的节奏是”顺手迁”:开发某个功能时,把该功能触及的文件顺带纳入当前批次。这样类型覆盖率随业务迭代自然上升,不需要专门的迁移排期。判断某个文件值不值得迁,和用 AI 重构遗留代码的思路一致——看修改频率、复杂度和被依赖程度,三者都低的文件可以先不动。


  1. 确定本批文件清单(≤10 个文件),写入 MIGRATION.md,记录当前 tsc 报错数。

  2. 补依赖的 @types 或最小 shims 声明,保证报错只来自本批文件。

  3. 把文件清单 + 不改公共签名的边界条件一起交给 AI,要求逐文件输出改动说明。

  4. 跑 npx tsc –noEmit 对比报错数,跑一次完整测试套件确认行为未变。

  5. 提交 PR,diff 控制在可评审规模;合并后更新 MIGRATION.md 的三个基线数字。

和其他 AI 编程流程怎么衔接

迁移过程中会顺带遇到两类问题:依赖版本不兼容,以及 AI 生成代码引入的新风险。前者可以参考用 AI 做依赖升级的完整流程,后者建议在每批 PR 里固定跑一遍AI 生成代码安全审查清单。如果你习惯让 AI 先出方案再动手,那么迁移这种大改动尤其需要Plan/Act 分离的工作流——先让它列出分批计划,你确认后再逐批执行。


老项目迁 TypeScript 一定要全部转成 .ts 文件吗?
不需要。allowJs 开启后 .js 可以长期保留,只要在 .d.ts 或 JSDoc 里给出类型即可。真正需要重命名的是”正在被高频修改”的文件,其余保持原样不影响类型收益。

strict 应该一次全开还是逐项开?
逐项开。推荐顺序是 strictNullChecks → noImplicitAny → strictFunctionTypes → 其余。strictNullChecks 收益最大、也最容易暴露真实缺陷,优先开它性价比最高。

AI 生成的大量类型定义可信吗?
需要分层看:从真实数据样本反推的接口定义可信度高;对第三方返回结构的猜测可信度低,必须对照文档或抓一段真实响应校验。凡是 AI 写的”我认为应该是这个结构”,都要降级为待验证。

迁移期间报错数反而变多了怎么办?
这是信号而不是失败——通常说明该批文件依赖的底层还没迁完。应该暂停本批,回头先把被依赖的模块纳进下一批,而不是在本批里用 any 把错误压下去。

一句话收尾:TypeScript 迁移不是一次改造,而是一条持续收敛的曲线。把基线数字、分批顺序、 AI 的改动边界这三件事固定下来,曲线就会稳定向下。

这篇有帮助吗?
云上的幻象
云上的幻象查看主页

七彩云博客,分享 WordPress 建站实战与 AI 工具测评,覆盖服务器运维、站长工具、软件资源与电商运营干货,专注原创实用的主题插件、网站加速与安全优化教程。

968文章4评论

相关文章

评论 (0)

欢迎你,新朋友,感谢参与互动!文明发言,理性交流 · 首次评论将在审核后展示