做了几年前端基础架构,我最怕听到一句话:“这个JS项目,找个时间迁到TS吧。”TypeScript本身不难学,难的是一个几千文件的老JS代码库里类型标注基本为零,跑起来全靠黑盒。隐式any、动态属性、各种运行时拼装的对象,每一个点都要靠人脑里的“项目记忆”去补全。最近我用Claude Code把整套迁移流程重新跑了一遍,发现AI辅助能大幅压缩前期调研和标注的工作量,尤其是从JS到TS这种大规模类型迁移,它比传统的人工逐文件修改高效太多。这篇就把我实测的完整流程、tsconfig配置方案、Claude Code的具体用法和踩过的坑一次讲清楚,正在准备做类型迁移的团队和个人可以直接照做。
1. 大规模JS项目迁移TS,真正的难点在哪
1.1 迁移不是“改后缀名”那么简单
很多团队启动TS迁移时的第一反应是:引入TypeScript依赖、配置一个tsconfig、把所有.js文件改成.ts,然后等着编译器告诉自己哪里错了。真实情况是,改完扩展名第一次跑tsc,报错数量往往直接上千,现场立刻变成大型劝退现场。
原因在于老JS代码的隐式类型推断大量依赖运行时上下文。举个例子,一段很常见的配置读取代码:
js复制// 原JS代码
function getConfig(key) {
const store = JSON.parse(localStorage.getItem('app_config') || '{}');
return store[key];
}
这个函数在JS里跑得毫无问题,但改成TS之后,JSON.parse返回的是any,store[key]返回的也是any,整个函数等于没有类型保护。你迁了文件,却什么都没得到。类型系统是结构化的、静态的,它需要代码里出现明确的“形状”,而老JS恰恰在“形状”这件事上最模糊。
另外还有个容易忽略的问题:老项目往往存在大量跨模块的隐式依赖。一个工具函数被几十个地方调用,你以为它只接收字符串,实际上有人传了对象、有人传了undefined、还有人动态拼接字段。一旦迁移时把签名收窄,所有调用点一起爆红。这种“改一处、炸一片”的连锁反应,才是大规模迁移真正的痛苦来源。
1.2 传统迁移路线的三个硬伤
我经历过纯人工的迁移项目,也带过团队做。回顾下来,传统路线有三个绕不开的硬伤:
第一,上下文依赖难以追踪。一个函数的定义和调用点往往散落在几十个文件里,排查哪条调用路径会触发哪种类型错误,非常消耗精力。就算你开了tsc --noEmit,编译器只告诉你“这里类型不匹配”,不会帮你梳理调用链。
第二,存量错误与新增代码互相干扰。迁移到一半时,仓库里同时存在JS和TS文件,报错既有老代码的历史遗留,又有新迁移引入的问题。每一轮编译跑出来的几百条错误里,真正需要立刻处理的可能只有十分之一,剩下全是噪音。
第三,人力密集、反馈周期长。人工补JSDoc、人工改类型断言、人工核对每一个any,这些都是高重复度的机械劳动。一个中等规模的模块至少需要半天到一天来处理,期间还要不断切换编辑器、终端、测试报告,大脑负担极重。
这三个硬伤决定了纯靠堆人力去搞大规模类型迁移,成本非常不可控。这也是为什么我觉得需要把AI工具引入流程。
1.3 Claude Code在迁移流程中扮演什么角色
Claude Code是Anthropic推出的命令行编程代理,它能在终端里读取项目文件、执行命令、修改代码。和普通的对话式ChatGPT不同,它直接工作在你的项目目录里,可以拿到真实的代码上下文。
在我设计的迁移流程里,Claude Code承担三种角色:
- 情报员:读取文件和依赖图,梳理模块关系,定位高频any和隐式any集中区。
- 执行者:批量补JSDoc、批量改类型、批量修复常见TS错误,把机械劳动快速消化。
- 守门员:每完成一批修改,自动运行
tsc、lint和测试,把新增报错抓出来继续修。
但它不承担决策者的角色。类型模型怎么设计、哪些any可以保留、哪些边界情况需要人判断,这些依然要人来定。把AI定位成“高产出实习工程师”而不是“全自动迁移机”,整个流程会稳很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的准备工作:环境、基线与战略
2.1 Claude Code安装与项目接入
Claude Code的安装方式比较直接,任选一种即可:
bash复制# 方式一:通过npm全局安装
npm install -g @anthropic-ai/claude-code
# 方式二:使用官方安装脚本
curl -fsSL https://claude.ai/install.sh | bash
安装完成后,在项目根目录执行:
bash复制cd your-project
claude
就会进入交互式终端。Claude Code目前有命令行、桌面端和VS Code插件三种形态,做迁移这种重活我个人强烈推荐CLI,因为整个流程会和tsc、git、npm完全串在一起,反馈链路最短。桌面端适合可视化预览,VS Code插件适合边看代码边对话,但论“读取项目+执行命令+自动改文件”的完整度,CLI始终是最强的。
注意:项目首次接入时,建议先让Claude Code花点时间读一遍
README.md、package.json、已有的配置文件,建立项目背景认知。不要上来就丢一堆任务给它,否则它只能靠猜测干活。
另外,Claude Code支持自定义技能(skills),可以针对迁移场景定制一套“类型标注员”“错误排查员”之类的行为规范,把团队的迁移规范固化到技能文档里。后面我会讲到怎么用,这里先记住有这回事。
2.2 摸清家底:JS文件与依赖关系扫描
迁移前一定要先搞清楚项目里有多少JS文件、分布在哪些目录、彼此的依赖关系如何。我习惯用这几条命令快速建立数据基线:
bash复制# 统计项目JS文件总量
find src -name '*.js' | wc -l
# 查看JS文件在各目录的分布
find src -name '*.js' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -20
# 用madge生成模块依赖图
npx madge --image deps.png src/
也可以用Claude Code直接读目录结构,让它输出一份模块依赖分析报告:
text复制请扫描 src 目录下所有 JS 文件,按目录列出文件数,并用Markdown表格输出:
1. 被依赖次数最多的TOP20文件
2. 完全没有内部依赖的叶子模块
3. 存在明显循环依赖的模块组
结果保存到 migration-notes/module-map.md
为什么要从依赖关系入手?因为迁移顺序必须“自底向上”。底层工具函数、常量定义这类无依赖或低依赖的模块,先迁先稳;之后再迁移被大量调用的业务模块;最后处理入口和初始化逻辑。如果反着来,上层模块先迁完,底层还是any,上层类型等于架在沙地上,推倒重来的概率极高。
2.3 tsconfig渐进式配置:迁移的制度保障
tssconfig是整个迁移工程的地基。我强烈建议不要一上来就开strict,而是采用“渐进式严格”策略。下面这份配置是我在迁移项目初期使用的:
json复制{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"esModuleInterop": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo/migrate.tsbuildinfo"
},
"include": ["src"]
}
各选项的意义我展开说一下:
allowJs是迁移期的关键开关,它让TypeScript编译器把.js文件也纳入整个编译范围。这样.js和.ts可以共存,不会因为仓库里还有JS文件就报“无法找到模块”。checkJs让编译器主动检查JS文件里的类型问题,但它默认不会对隐式any报错,正好适合作为“温和的提醒器”。noEmit只做类型检查,不产出编译产物,避免污染构建流程。incremental配合tsBuildInfoFile做增量编译,大幅缩短重复tsc的时间。迁移过程中你可能要反复跑几十轮检查,这个优化非常值。skipLibCheck跳过node_modules里声明文件的检查。第三方库的类型定义有些很粗糙,不跳过的话会产生大量自己无法控制的噪音。moduleResolution: bundler适合现代Vite、Webpack项目,兼容TS的模块解析策略。
用表格概括三个阶段的状态:
| 阶段 | allowJs | checkJs | strict | 目标 |
|---|---|---|---|---|
| 阶段一:基线期 | true | true | false | 让所有JS进入类型检查范围,量化错误规模 |
| 阶段二:标注期 | true | true | false | 用JSDoc补类型,逐步消除显性错误 |
| 阶段三:收尾期 | false | false | true | 全部文件转为TS,开启严格模式 |
这份配置就是迁移的“制度保障”,后续所有操作都围绕它推进。
3. 核心实操:用Claude Code走完五步迁移
3.1 第一步:checkJs基线建立类型安全底线
配置好tsconfig后,先跑一次检查:
bash复制npx tsc --noEmit > migration-notes/baseline-errors.txt 2>&1
这一步的目的是量化问题规模,而不是立刻修错。拿到基线后,把错误清单交给Claude Code做初步分析:
text复制我已在 src 下开启 checkJs,tsc 报错文件在 migration-notes/baseline-errors.txt。
请帮我完成以下事情:
1. 按错误代码(TS开头)统计分布,列出TOP10
2. 对每个TOP错误代码,从项目里找出3个典型报错位置,读取对应文件
3. 分析这些错误的主要成因:是缺少JSDoc、隐式any、还是第三方库缺类型?
4. 将分析结果写入 migration-notes/error-report.md
Claude Code最大的价值在这里体现出来了:它不只是输出建议,而是真的会去读取报错文件里提到的源码位置,再结合错误代码分析成因。传统流程里这一步需要工程师自己逐个打开文件看,现在AI可以先把错误归类,人只需要看报告就能确定下一步计划。
有一点要提醒:CLI模式下Claude Code的输出会受上下文窗口限制,一次性让它处理上千条报错容易中途截断。解决办法是让结果直接写入文件,而不是在对话里长篇打印;或者按目录分批分析。
3.2 第二步:JSDoc批量标注,让JS先“有类型”
很多团队会纠结一个问题:为什么要先在JS文件里写JSDoc,不直接把扩展名改成.ts?原因在于,直接改扩展名后,所有隐式any都会立刻变成编译报错,压力集中爆发;而先在.js文件里用JSDoc把类型标好,改扩展名时大多数类型信息依然有效,转换非常平滑。
JSDoc标注的核心语法非常简单:
js复制/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {number} age
*/
/**
* 获取用户信息
* @param {string} userId
* @returns {Promise<User>}
*/
async function getUser(userId) {
const res = await fetch(`/api/users/${userId}`);
return res.json();
}
复杂类型可以通过import()语法在JSDoc中引用:
js复制/** @type {import('./types').Config} */
const defaultConfig = { theme: 'light', locale: 'zh-CN' };
Claude Code完全可以胜任批量标注工作。我实际用下来的Prompt模板是:
text复制请为 src/utils/ 目录下的所有 .js 文件添加完整JSDoc类型标注。
硬性要求:
1. 不改变任何运行时行为
2. 函数必须标注 @param 和 @returns
3. 复杂对象用 @typedef 定义,不直接堆在注释里
4. 修改后运行 npx tsc --noEmit,确认没有新增错误
5. 每处理完一个文件,在对话里用一句话总结
这里要特别强调“不改变运行时行为”。AI在补类型时偶尔会手滑把逻辑也调了,比如顺手把一个==改成===、把数组遍历方式改了。所以Prompt里必须带限制词,并且每批修改后都要跑一次tsc和测试来验证。
批量标注阶段也是训练AI理解项目规范的好时机。如果项目里有统一的分页返回结构、统一的错误码对象,可以在JSDoc里定义对应的@typedef,让AI反复复用,而不是每个文件各自造一套。
3.3 第三步:自底向上的文件扩展名迁移
当一个文件的JSDoc标注到位,就可以正式从.js改成.ts了。我一直用git mv,这样改动记录一直保留在版本历史里,审计和回滚都方便:
bash复制git mv src/utils/format.js src/utils/format.ts
改完后立刻跑类型检查,看新增了哪些错误。如果没有新增错误,说明JSDoc阶段已经把类型信息补全了;如果有,通常是调用方的类型约束还没到位,需要把调用方也纳入本轮处理。
这一步的关键是迁移顺序。我建议给Claude Code一个明确的排序任务:
text复制读取 src/utils 下所有 JS 文件,分析它们之间的依赖关系。
请输出一份迁移计划表,包含:
1. 文件路径
2. 内部依赖数量
3. 建议迁移顺序编号(从无依赖的底层文件开始)
4. 每个文件预计需要修改的类型点数量
然后在执行阶段,让AI分小批推进:
text复制按照你刚才给出的迁移计划,从第1个文件开始,每次处理不超过5个文件。
每处理完一个文件,运行 npm run typecheck。
如果有新增报错,读取报错位置并修复,直到 typecheck 零新增错误再进入下一批。
分批处理非常重要。一次性让AI改几十个文件,任何中间环节出错都很难定位;小步快跑、每步验证,才是可控的节奏。
3.4 第四步:any治理与精确类型收窄
大部分文件转为.ts之后,紧接着就是any治理。你会发现代码里的any分成两类:隐式any和显式any。隐式any可以在tsconfig里逐步打开noImplicitAny来暴露,显式any则需要一次专项清理。
我先让Claude Code做一次全库审计:
text复制扫描 src 下所有 .ts 文件,统计显式 any 的数量。
按文件分组,输出每个 any 所在的行号和上下文。
特别标注出风险高的位置,例如函数参数、返回值、对外接口定义。
先不要修改,把清单写到 migration-notes/any-audit.md。
拿到清单后,人力来定优先级:对外暴露的API、核心业务状态、被大量调用的工具函数优先处理;遗留的临时脚本、一次性逻辑可以排到后面。
收窄any类型时,我常用的TS技巧就派上用场了。典型场景是动态配置对象,可以用keyof typeof把字符串键约束起来:
ts复制const CONFIG_KEYS = ['theme', 'locale', 'timezone'] as const;
type ConfigKey = typeof CONFIG_KEYS[number];
function getConfig(key: ConfigKey): string {
const store = readStore();
return store[key];
}
再比如对unknown类型的运行时数据,用类型守卫来收窄:
ts复制function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
收窄类型的过程中,AI可以承担重复性较高的改造,但涉及业务语义判断的地方,人一定要参与。比如某个字段在该业务场景里是否真的永远存在,这种问题AI无法从代码里得到答案。
3.5 第五步:回归验证与严格模式收尾
当大部分文件都迁移完后,就到了“上强度”环节:逐步开启严格模式。这不是一步到位,我一般按照strictNullChecks、strictFunctionTypes、noUncheckedIndexedAccess、最后strict: true的顺序推进。
每打开一个配置项,都会出现一批新错误。这时候Claude Code适合做“批量修复执行者”:
text复制我在 tsconfig.json 中打开了 strictNullChecks。
请运行 npx tsc --noEmit,读取所有新增报错并按文件分组。
对每个报错,读取对应文件,分析是真实空值风险还是类型定义过窄,
修复后运行 npm run typecheck && npm run lint && npm test,
直到全部通过。不要用 @ts-ignore,不要用 as any 绕过。
注意Prompt最后的禁止项。AI在高压下会走捷径,@ts-ignore和as any是最常见的偷懒手段。如果允许它用这些,迁移质量会大打折扣。
在这个阶段,CI里的类型检查也应该成为硬性门禁。我在package.json里固定了三个脚本,要求每次提交前都过一遍:
json复制{
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src --ext .ts,.js",
"test": "vitest run"
}
}
任何一次迁移提交,只要typecheck失败就不能合并。这根红线保住的是整个迁移工程的底线。
4. Claude Code使用技巧:如何让AI输出高质量迁移结果
4.1 上下文管理:从一个文件到一个模块
Claude Code的上下文窗口不是无限的,大型项目动辄上千个文件,你不能指望它在一次对话里记住所有代码。我的做法是分层建立上下文:
第一层,项目级。在项目根目录准备一个CLAUDE.md文件,把“项目结构、TS规范、常用命令、禁止事项”写在里面。Claude Code每次启动时会自动读取这个文件,相当于给它一份项目说明书。
我的CLAUDE.md大概长这样:
markdown复制# 项目背景
- 这是一个电商后台管理系统,前端构建工具是Vite。
- 代码在 src 目录,业务代码按照 modules 组织。
- 当前正在进行 JS 到 TS 的迁移,迁移计划见 migration-notes/。
# 迁移规范
- 禁止使用 @ts-ignore。
- 显式 any 必须有注释说明原因,否则视为不合格。
- 新迁移的 TS 文件必须通过 npm run typecheck。
- 优先使用类型守卫和联合类型,而不是类型断言。
# 常用命令
- npm run typecheck:类型检查
- npm run lint:代码规范检查
- npm test:单元测试
第二层,模块级。真正开始处理一个模块时,先让Claude Code读该模块的入口文件和核心类型定义,再让它处理具体文件。不要一上来就丢一个超大目录让它“全看了”。
第三层,文件级。对于单个大文件,拆成“先读结构,再读实现,再动手改”三个步骤,避免一次性灌入过多代码导致AI顾此失彼。
4.2 写Prompt的几种有效范式
经过多轮尝试,我总结出几类在迁移场景下最有效的Prompt范式,按用途分类列出来:
评估类范式:
text复制请评估 {文件路径} 的迁移复杂度。
参考它直接依赖的模块,用表格列出:
隐式any数量、显式any数量、外部依赖数、建议迁移步骤。
先输出评估结果,不要修改代码。
这类Prompt适合在迁移计划阶段使用,让AI先“看”再“动”。把“不要修改代码”写进去,能防止AI自作主张提前动手。
修改类范式:
text复制请为 {文件路径} 补充JSDoc类型标注。
保持语义完全不变。
修改后运行 npx tsc --noEmit 检查,如有新增错误逐条修复。
最后用代码块总结本次新增了哪些类型定义。
修改类Prompt的核心是明确边界:“保持语义完全不变”是红线,“运行检查”是验证方式,“总结新增类型”是让结果可审计。
审计类范式:
text复制扫一遍 {目录},找出所有 as any 强制转换和类型断言。
列出文件、行号、上下文,判断哪些是安全的、哪些有风险。
只输出审计报告,不要做任何修改。
这类Prompt的价值在于:AI可以把危险代码定位出来,人再决定怎么处理。它不直接改,就不会引入新的风险。
批量类范式:
text复制对 {目录} 下所有 .js 文件执行以下流程:
1. 分析文件间依赖关系
2. 按从底向上的顺序迁移到 .ts
3. 每迁移一个文件运行 npm run typecheck
4. 固定每次最多处理5个文件,每轮结束向我汇报
批量任务一定要设“轮次上限”和“检查间隔”,让AI在可控范围内行动。没有边界的批量任务,最后很可能改出一堆需要返工的东西。
4.3 自动改代码后的人工审查策略
Claude Code再强,我也不建议“改完直接提交”。每次AI修改后,我至少会做三件事:
第一,看git diff。重点关注签名变化和逻辑变动。AI在补类型时偶尔会把参数顺序调整、把返回值从string改成string | undefined,这些都是肉眼可见的变更,必须在diff阶段发现。
第二,要求AI给出修改摘要。我在Prompt里经常加一句“用代码块总结本次改动”,这样在review时不用自己逐行比对,先看摘要再抽查细节。
第三,跑真实的测试。类型检查和lint只覆盖静态层面,运行时行为是否正确,必须靠测试兜底。迁移中我特别关注测试覆盖率有没有下降,如果AI因为“类型问题”改动了某些边界逻辑,测试会直接暴露。
还有一条铁律:核心业务模块的类型模型,必须在迁移前由人来定义,而不是让AI自己发挥。我会先给AI一份“类型契约”,例如“订单状态是一个联合类型:'pending' | 'paid' | 'canceled'”,再让它按照这个契约去改代码。AI在类型设计上并不能替代人对业务的理解。
5. 迁移过程中的典型问题与排查实录
5.1 高频TS类型错误速查
整个迁移过程中,以下错误代码出现的频率最高。我把它们整理成速查表,遇到时可以直接按图索骥:
| 错误代码 | 含义 | 常见场景 | 快速处理思路 |
|---|---|---|---|
| TS7006 | 参数隐式any | 回调参数、函数参数没有类型 | 让Claude Code读取调用点,推导参数类型并补JSDoc |
| TS7053 | 索引表达式无隐式索引签名 | 动态取对象属性 | 把对象类型改为Record<string, T>或补充索引签名 |
| TS2339 | 属性不存在 | 第三方库类型缺失、访问动态属性 | 安装对应@types/包,或定义接口收窄 |
| TS2345 | 参数类型不匹配 | 调用签名与定义不符 | 检查调用点统一类型,优先让类型“向上兼容” |
| TS2322 | 赋值类型不匹配 | 类型断言失效、返回类型过窄 | 用类型守卫或交叉类型扩展类型 |
| TS2531 | 对象可能为null | 开启strictNullChecks后高频出现 | 增加空值判断,或使用可选链?. |
对每个错误代码,我的处理原则是“先定位再修复,不掩耳盗铃”。最不推荐的处理方式是用as any或@ts-ignore强行压掉报错,这样等于把迁移成果全部打了折扣。
5.2 Claude Code使用中的若干坑
用了Claude Code做迁移之后,我踩过几个挺有代表性的坑,写出来给大家避雷:
第一个坑是上下文截断导致的“半截修改”。处理超大文件时,AI可能会在输出中途中断,导致文件被截断或者改了一半。后来我规定单个文件如果超过500行,一律拆分成“读结构、读实现、动手改”三步,并且每步都确认完成再继续。
第二个坑是AI的“顺手优化”。明明只让它加类型,它却顺手把for循环改成了map、把字符串拼接改成了模板字符串。虽然这些改动本身不算错,但在迁移期间混入无关变更会让review变得很困难。我的应对方式是在Prompt开头就写明“只处理类型,不做任何逻辑重构”,每次修改后严格看diff。
第三个坑是增量编译缓存失效。开启了incremental之后,偶尔会出现tsc报错但代码看起来明明没问题的情况,多半是.tsbuildinfo缓存异常。直接删除tsbuildinfo目录再跑一次就能解决。Claude Code遇到这种诡异问题时,我也学会让它先检查缓存而不是反复改代码。
第四个坑是AI对动态特性的误判。老JS项目里大量使用动态属性、getter/setter、原型链扩展,这些在TS里都很难表达。当AI给出的“修复方案”明显绕弯时,我就知道它没有真正理解运行时逻辑,这种情况必须人工介入,而不是接受它的“一招绕过”。
5.3 团队协作下的迁移节奏与规范
如果你是团队作战,那迁移工程还需要额外的协作制度保障。我建议坚持以下几个原则:
迁移工作放在独立分支,每个PR只处理一个模块或一个目录,控制diff规模。我见过有人一个PR里塞了50个文件的迁移,reviewer看都看不完,最后只能无脑合并,风险极高。
CI里加类型检查门禁,任何引入新@ts-ignore或显式any的提交都不能通过。这个门禁的意义在于防止迁移成果回潮。
每个PR至少有一个熟悉该模块的人做code review,重点不是看类型注解本身,而是看AI是否在“改类型”的名义下改动了业务逻辑。
迁移期间不建议同时开展大规模重构。有一次团队一边迁移一边升级依赖库,结果类型错误和业务bug混在一起,定位问题花了两倍时间。迁移最好是一个纯粹的动作,不要和其他大改动叠加。
最后,迁移完成不等于项目“完全TS化”。真正的收尾是让团队的新代码都按照TS规范来写,并且在PR模板里加上“本次是否引入新any”的检查项,让规范变成习惯。
最后说点我这几轮项目下来最真实的感受。Claude Code再快,它也只是帮你把重复劳动压缩了,真正决定迁移质量的还是人——在动手之前对模块边界的划分、对类型模型的定义、对“哪些地方值得花力气精确”的判断。我的习惯是把AI当作一个高产的实习生,给它足够的背景说明、明确的验收标准,然后对每一次改动认真review。如果你正打算启动一次大规模类型迁移,别想着一步到位开strict,先把基线建立起来,让AI帮你把那些无脑标注和机械修复的工作消化掉,你会明显感觉到余力多了很多,后面真正处理有挑战的类型设计时才不会手忙脚乱。
