说实话,接手过老旧 JS 项目的人都懂,从 JavaScript 迁到 TypeScript 这件事,看着是“加个类型标注”,真正动起手来完全不是那么回事。几千个文件、几十万行业务代码,手动补类型能把人补到怀疑人生,而且大部分时间都花在机械劳动上——根据函数用法推导参数类型、给接口补结构定义、把 require 改成 import。这种活儿技术含量不高,但量极大,恰恰是 Claude Code 这类 AI 编程工具最擅长消化的场景。
这篇文章我从头到尾梳理一遍,怎么用 Claude Code 辅助完成一次大规模 JS 到 TS 的类型迁移。不是纸上谈兵,是这几天刚在一个真实业务项目上完整跑过一遍的流程,包括环境准备、迁移策略、提示词怎么写、遇到哪些坑、哪些地方必须人工兜底,全部摊开讲。
1. 迁移之前:先想清楚“为什么非要用 Claude Code”
1.1 类型迁移的真实痛点在哪
很多人以为 JS 到 TS 迁移的核心难点是“不会写类型”。真不是。TS 的语法学起来很快,interface、type、泛型这些概念,认真看两天文档基本能上手。真正折磨人的是另外三件事。
第一是工作量。一个中型项目动辄几百个模块,每个模块都要梳理入参出参、对象结构、回调签名。人工做的话,平均一个文件快则十几分钟,慢则半小时以上,几百个文件就是几十个小时的纯体力活。第二是上下文断裂。类型标注不是孤立地看一个函数就能写对的,你得知道这个函数在哪些地方被调用、调用方传入什么、返回值被怎么使用,纯靠人工逐个文件跳转翻阅,思维负担极重。第三是边角情况多。JS 项目里动态属性、可选链、默认参数、arguments、回调函数、this 指向这些写法到处都是,每处理一种都要额外判断。
Claude Code 的价值恰好落在这三点上:它能读整个项目目录,理解跨文件的调用关系;它能一次性处理大批文件的批量改写;它在上下文窗口内能记住之前处理过的模块定义,减少重复说明。说白了,它不是帮你发明新东西,而是把“根据上下文批量补类型”这件事的效率拉高了一个量级。
1.2 Claude Code 适合做什么、不适合做什么
先泼盆冷水。Claude Code 不是万能的,迁移这件事上它有明确的能力边界。
适合做的:批量生成 interface 和 type 定义、根据函数体推导参数与返回类型、把 JSDoc 注释转成正式类型标注、处理 require 到 import 的模块语法转换、生成 tsconfig 配置、处理常见第三方库的声明引用。这些任务模式清晰、重复性高,效果非常稳定。
不适合做的:涉及复杂业务含义的命名决策。比如一个字段到底该叫 status 还是 state,这需要懂业务;比如一个函数的返回值在不同业务分支里结构差异很大,统一成联合类型还是拆开处理,这需要架构判断。AI 能做建议,但决策必须由人来定。另外,改完之后的正确性验证也别指望它,类型检查过了只是第一步,运行时不报错才是真通过。
所以整个迁移策略我定为:Claude Code 负责 80% 的机械标注和结构推导,人负责 20% 的架构决策和最终验证。这个比例在实操中比较健康。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的地基:项目体检与环境准备
2.1 给项目先做一次静态体检
拿到项目别急着让 Claude Code 开干,先人工做一轮体检。原因很简单:AI 工具输出的质量高度依赖输入信息的清晰度,如果项目本身目录混乱、依赖不明确,它写出来的类型定义也会歪。
我这边体检主要看五个维度:
- 项目规模:统计 JS 文件数量、总代码行数、模块依赖深度。文件数量决定迁移策略,几十个文件可以激进一点,几百个文件必须分阶段。
- 入口结构:确认
package.json、主入口文件、构建脚本的位置,这些是 Claude Code 理解项目拓扑的锚点。 - 依赖清单:看
package.json里的 dependencies 和 devDependencies,标记哪些第三方库有官方类型包,哪些没有。没有的库是迁移中的大坑,需要提前规划。 - 现有 JS 风格:项目里是否已经有 JSDoc 注释、是否使用了 Flow、代码里
any的隐式程度。有 JSDoc 的项目迁移难度会低很多,Claude Code 可以直接把注释转成类型。 - 特殊文件:
.d.ts文件、全局声明、环境变量注入、动态import、require.context这类 Webpack 特有写法,都得先列出来。
体检结果最好整理成一份简单的说明文档,后续不管是自己参考还是喂给 Claude Code 做上下文,都有据可依。
2.2 把 Claude Code 接入项目工作流
安装和接入方面,Claude Code 目前是通过命令行和编辑器插件两种方式使用。推荐的方式是直接在项目根目录启动 CLI,这样它能以项目根目录为工作区,读取文件结构和内容。安装过程本身不复杂,但有几个细节值得注意。
第一,建议在项目根目录下启动,别在子目录里启动。Claude Code 的上下文感知范围很大程度取决于当前工作目录,根目录启动它能看到的文件范围最完整。第二,项目里的 .gitignore 要提前把 node_modules、构建产物目录、.tsbuildinfo 这类文件加进去,避免 AI 在扫描时被海量无关文件干扰。第三,如果项目有 ESLint 或 Prettier 配置,最好保留着,Claude Code 生成代码时会参考这些配置来匹配代码风格。
一个实操中的小技巧:在项目根目录放一个 CLAUDE.md 文件,把项目背景、目录结构、常用命令、编码规范写进去。Claude Code 会自动读取这个文件作为项目级上下文,后面每次交互都带着这些信息,省去反复交代背景的麻烦。
2.3 确定迁移顺序和边界
迁移顺序决定了整个过程是顺畅还是处处踩雷。我采用的顺序是“自底向上、依赖驱动”:先迁移没有依赖其他模块的纯工具函数和常量定义,再迁移被大量引用的基础模块,最后迁移业务页面和入口文件。这个顺序保证任何时刻项目都是可编译的——底层的类型先立起来,上层文件迁移时才能引用到正确的类型。
边界划分也很重要。不是所有文件都需要一口气迁移完。迁移期间项目还要继续开发,所以我会把“本次迁移范围”和“后续迁移范围”分开,先聚焦一批核心目录。比如这次只迁 src/utils、src/services、src/types 三个目录,其他目录留到下一轮。这样每次改动范围可控,回归测试的压力也小。
3. 核心实操:让 Claude Code 承担 80% 的类型标注工作
3.1 第一步:生成 tsconfig 与基础类型骨架
迁移第一步不是写类型,而是搭配置。tsconfig.json 是 TS 项目的总开关,配置不对后面全是折腾。
我让 Claude Code 生成 tsconfig 的提示词大概这样:
code复制请为这个项目生成一份 tsconfig.json 配置。
项目是一个 [Vue2/React/Node] 项目,源码在 src 目录,
当前从 JavaScript 迁移到 TypeScript,希望采用渐进式迁移策略。
要求:
1. 开启 allowJs 和 checkJs,允许 JS 文件继续存在
2. 开启 incremental,加速增量编译
3. 暂不开启 noImplicitAny,避免一开始报错太多
4. strict 先设为 false,等类型覆盖率上来后再逐步开启
5. 引入 @types/node 和项目中已有的第三方库类型
allowJs 和 checkJs 这两个选项是渐进式迁移的灵魂。开启后,JS 和 TS 文件可以共存,TS 编译器会检查 JS 文件但默认不报严重错误,这样项目可以保持可运行状态,一批一批地迁移。
生成基础类型骨架的提示词则是:
code复制请阅读 src 目录下的所有文件,识别出项目中重复出现的对象结构、函数签名、
通用回调类型,为它们生成一份 types/index.d.ts 文件。
重点关注:
- API 接口返回的数据结构
- 通用的配置对象结构
- 跨模块共享的事件回调类型
这一步输出的 index.d.ts 是整个迁移的地基,后续所有文件迁移时都能引用这里定义好的类型。
3.2 第二步:从入口文件开始让 Claude Code 逐模块标注
配置就位后进入核心环节:逐文件标注类型。
实操方式有两种,一种是让 Claude Code 自己扫描整个目录批量处理,一种是一个文件一个文件地对话处理。项目文件多的时候,批量处理看起来效率高,但实际效果不稳定——生成结果容易“想当然”,在没有严格上下文约束的情况下补出错误类型。我最终采用的是“文件组”方式:把同一模块下互相关联的几个文件放到一次会话里处理,既保留上下文关联性,又不至于因范围过大导致输出失控。
提示词模板:
code复制请将以下文件从 JavaScript 迁移为 TypeScript:
文件列表:
- src/services/api.js
- src/services/user.js
- src/utils/format.js
要求:
1. 为文件中定义的所有函数补充参数类型和返回类型
2. 识别对象字面量的结构,生成 interface 或 type 定义
3. 不要改变任何函数名、变量名、导出方式
4. require 改为 import 语法
5. 对不确定的第三方库可以使用 any,并在注释中标记 TODO
这里有个关键点:“对不确定的第三方库可以使用 any”。迁移初期不要追求 100% 类型安全,那会让 Claude Code 卡在某些角落反复纠结。先用 any 把流程走通,后面有专门一轮来清理 any 和其他类型漏洞。
3.3 第三步:处理外部依赖与全局变量
第三方库是迁移中最大的变量。有些库自带类型定义,比如 lodash 有 @types/lodash,express 有 @types/express,直接安装对应的 @types 包就行。但很多库没有官方类型,比如一些内部私有的 npm 包,或者历史悠久的小众库。
没有类型的库,Claude Code 会默认把它当成 any 处理,这在 strict 模式下会报错。解决办法是让 Claude Code 在 src/types 目录下生成一个 module-name.d.ts 声明文件:
typescript复制declare module 'legacy-lib' {
export function init(options: Record<string, unknown>): void;
export function getConfig(key: string): unknown;
export const version: string;
}
只声明项目中实际用到的函数和变量,不用把整个库都声明一遍。这样既不阻塞迁移,又保留了最基本的使用约束。
全局变量和 window 扩展属性也是 JS 项目里常见的东西。比如 window.__INITIAL_STATE__、window.g_config 这种。处理方式是为它们生成一个 global.d.ts:
typescript复制export {};
declare global {
interface Window {
__INITIAL_STATE__: Record<string, unknown>;
g_config: {
apiBaseUrl: string;
env: 'development' | 'production';
};
}
}
这一步做完,Claude Code 迁移出的文件里就不会再因为 window 上挂载的属性而犹豫不决了。
3.4 批量处理模板代码和重复结构
业务代码之外,项目里往往还有一批高度模板化的文件:枚举常量、状态码映射、路由表、配置项。这些文件结构单一、重复性强,批量处理效率极高。
我给 Claude Code 的提示词:
code复制以下是项目中的常量配置文件,请将它们转换为 TypeScript:
文件列表:
- src/constants/status.js
- src/constants/error-code.js
- src/constants/event-name.js
转换规则:
- 对象字面量根据值推断联合类型
- 字符串常量使用 const enum 或 as const
- 保持键名和值的原有含义不变
as const 是迁移这类文件的利器,能把对象属性推断成字面量类型,提供更强的类型约束。不过要注意,转换后某些依赖动态键名的代码可能报错,需要同步修改调用处的索引方式。
4. 迁移中最容易翻车的几个场景
4.1 any 的三种藏身处
迁移后最让人头疼的就是各种“漏网之鱼”的 any。我总结了一下,任何的主要来源有三个。
第一是隐式 any。函数的参数没有类型标注,TS 编译器推不出来,只能当 any 处理。这种情况开启 noImplicitAny 后就会暴露出来。第二是第三方库的隐式 any。没有类型声明的库,引用时全部被降级成 any。第三种是回调函数的裸参数,比如 arr.map(item => ...) 里的 item,如果 arr 本身是 any 类型,item 也会跟着变 any。
处理它们的方式分两步。先让 Claude Code 全局搜索 : any 和隐式 any 的分布情况,然后逐个模块把能推导的类型补上。补不了的地方再人工介入,看业务逻辑推断类型,或者用泛型约束。
清理 any 的提示词:
code复制请扫描 src 目录下所有迁移后的 TypeScript 文件,
找出所有显式标注为 any 的地方和隐式 any 的参数,
按文件列表输出分析结果:
- 文件路径
- 行号
- any 出现的位置(参数/返回值/变量/属性)
- 该位置可能的类型推断建议
对于推断建议,基于调用处的实际用法来判断,
不要盲目使用 unknown 替代 any。
4.2 类型交叉、联合与泛型的滥用
Claude Code 对类型工具有天然的热情,有时候会生成过度设计的类型。比如一个原本简单的参数对象,它可能给你搞成 Partial<A> & Omit<B, 'x'> & Record<string, string> 这种组合,读起来费劲,改起来更费劲。
遇到这种过度设计的类型,我的原则是:能简单就简单。一个函数参数就老老实实用 interface 定义,属性该可选就加 ?,一组固定值就用联合类型,没必要为了展示 TS 能力而堆叠类型操作符。
另外注意泛型的滥用。Claude Code 特别容易给简单函数套上泛型,比如一个 get(key) 函数也要 get<T>(key): T。这种泛型在没有严格约束的情况下就是隐形的 any,反而降低了类型安全性。人工审查时发现泛型没有实际约束意义,就要拆掉。
4.3 异步代码和回调里的类型
异步代码是类型迁移的老大难。Promise 的泛型推导在绝大多数情况下没问题,但一旦遇到嵌套回调、Promise.all 混用不同返回类型、或者事件回调里的异步操作,Claude Code 就容易给出错误的类型假设。
最典型的一个场景:setTimeout 和事件监听器里引用的外部变量。JS 代码里这些写法很随意,但 TS 下会产生类型收窄失效的问题。比如:
typescript复制let timer: number | null = null;
function start() {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
// 这里 TS 会认为 timer 是 number | null
// 因为在回调里做赋值会导致类型收窄失效
timer = null;
}, 1000);
}
Claude Code 处理这类场景时经常会在 timer 的类型上纠结,要么报错,要么干脆给个大范围的类型。遇到这种情况,我会手动改成明确的 let timer: ReturnType<typeof setTimeout> | null = null,让类型收窄更清爽。
4.4 严格模式要不要一轮开到位
很多人喜欢迁移完成后立即把 strict: true 打开,结果被海量报错淹没,然后又灰溜溜地关掉。说实话,这种“一步到位”的策略在中小型项目上可行,但上了规模的项目非常痛苦。
我建议分三步走:第一轮先把 allowJs 和 checkJs 打开,保证 JS 和 TS 并存;第二轮把迁移完成目录内的报错清零后,开启 noImplicitAny;第三轮在 all clear 之后开启 strict,一次性把剩下的严格性约束全部加上。
每一轮开启前,先让 Claude Code 跑一遍 tsc --noEmit,看报错数量和分布,做到心里有数再动手。
5. 常见问题与排查技巧实录
5.1 问题速查表
迁移过程中遇到的典型问题,我整理成了一张速查表:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
迁移后大量文件报 Cannot find module |
路径别名(@/)未在 tsconfig 中配置 |
在 compilerOptions.paths 中配置 @/*: ["src/*"] |
tsc 不检查 JS 文件 |
未开启 checkJs |
确保 allowJs 和 checkJs 都为 true |
| 迁移后函数行为变化 | 类型标注错误导致条件分支被推断收窄 | 回查变更部分的 diff,逐行对照确认 |
| 第三方库全部飘红 | 缺少类型声明 | 安装 @types 包或手写 .d.ts 声明 |
import 顺序被打乱 |
Claude Code 重排了解析顺序 | 用 ESLint 的 import/order 规则统一整理 |
迁移后运行时 undefined 错误 |
原 JS 动态属性在 TS 下被视为必选项 | 检查接口定义中属性是否有 ? 可选标记 |
大量 any 集中在某些文件 |
这些文件本身依赖过重、耦合度高 | 单独拆出来人工重构,或先临时跳过 |
这里面最危险的是“迁移后函数行为变化”。类型标注在理论上是编译期行为,不该影响运行结果,但当 AI 在迁移时“好心”帮你把一些写法规范化时,就可能引入行为差异。比如把 == 改成 ===、把 a || b 改成 a ?? b,这种改动对结果有微妙的影响。所以迁移后一定要跑一遍测试用例,没有测试的项目至少要把核心流程手测一遍。
5.2 让 Claude Code 更懂你项目的提示词技巧
用 Claude Code 做迁移,提示词的质量直接决定输出质量。几个实测有效的技巧分享下。
第一,上下文越具体越好。不要只说“帮我迁移这个文件”,要说“这个文件中的 fetchData 返回的数据结构包含 code、message、data 三个字段,data 在不同接口下结构不同”。Claude Code 强在吸收信息,弱在凭空猜测,喂给它信息它就能用好。
第二,善用对话历史。如果一批文件共享同一个类型定义,先让 Claude Code 定义好类型,再进行批量迁移,这样后续每个文件都会沿用已经建立好的类型体系。
第三,给你的约束编号。写提示词时把这个要求列成编号列表,比如“1. 不改变导出方式 2. 不改变函数名 3. 不确定的用 any 并标记 TODO”。Claude Code 对编号要求的执行率明显高于自然语言混杂的描述。
第四,遇到不听话的时候别硬顶着聊,把当前会话重置,重新写一份更明确的提示词。AI 工具一旦在一个错误方向上跑偏,继续对话很难拉回来,不如重启会话重新聚焦。
5.3 迁移完之后的类型覆盖率审计
迁移结束不代表完事,还需要做一轮类型覆盖率审计。TS 官方提供了 ts-coverage 工具,可以统计项目中 any 类型的占比和分布情况。这个数据很有参考价值,能客观反映迁移质量。
覆盖率低于 80% 的项目,建议继续补强类型定义;超过 90% 且 any 主要集中在边界场景(比如第三方库接口处),可以视为合格。实际的营收业务代码里,80%~90% 的覆盖率已经是相当好的状态了。
审计结果出来后,让 Claude Code 针对剩余的 any 生成处理建议,人再根据建议逐个排期清理。这个阶段不用急,把 any 清理作为日常重构的一部分慢慢消化就行。
6. 迁移之后的一些体会
整轮迁移跑下来,我的体会是:Claude Code 这类工具最大的价值不是“替代人写代码”,而是把项目中那些枯燥、机械、需要大量上下文的工作抢过去干,让人能够把注意力集中在真正需要判断力的事情上。
这个过程里有一点特别值得强调:它生成的东西,一定要当“初稿”看待。我见过有人把 AI 生成的类型定义直接提交,不 review,结果运行到凌晨线上爆了。类型定义看着对,但实际类型含义跟业务对不上,比没有类型的危害更大——因为虚假的类型安全感会诱发新代码写出更多错误假设。
另外,如果你的项目后续还会继续演进,建议在迁移时就顺手把类型定义按照业务域拆分。别把所有类型都堆在一个 index.d.ts 里,按模块拆开,后面的维护成本会低很多。还有一种更好的做法:类型定义直接写在对应的业务文件里,跟着代码走,而不是集中管理。这种内聚式的组织方式对中小型项目特别友好。
迁移完成后,我在项目里保留了一个简单约定:新代码一律用 TS 写,旧代码在每次改动时顺手补类型,“跟着业务走、改到哪迁到哪”的方式比单独拿整块时间去攻坚要平滑得多。毕竟,类型迁移是一段过程,不是一个终点。
