Cursor / Claude Code / Copilot 写完一段 TypeScript 直接 commit,你跑 tsc --noEmit 看到一堆 TS2322: Type 'string' is not assignable to type 'number' 或 TS2339: Property 'foo' does not exist on type 'unknown'。AI 模型没有运行时类型反射能力,只能凭训练数据猜接口形状,遇到 generics、union types、third-party .d.ts 时尤其容易出错。
最快的修法:用 npx tsc --noEmit --pretty false > ts-errors.txt 把完整错误列表导出,原文整段贴回,并明确要求”不准用 as any、不准加 @ts-ignore”,然后让 agent 每改一次就重跑一遍 tsc,直到错误数归零。下面拆 5 个高频根因,给出完整的自验证 agent loop,最后附一张错误码到修法的对照表。
先对号入座
看日志里第一个错误码,直接跳到对应根因:
| 你看到的第一个错误 | 最可能的根因 | 对应章节 |
|---|---|---|
库调用点报 TS2554 / TS2345 | AI 用了过时的 API 签名 | 根因 1 |
| 编译干净但运行时崩 | 某处 as any / as unknown as 把错盖住了 | 根因 2 |
错误信息里出现 never / unknown | 泛型参数漏写 | 根因 3 |
TS2531 / TS2532 / TS18047 | strict 模式下 null 没处理 | 根因 4 |
TS1484 或运行时 “X is undefined” | type 与 value import 混了 | 根因 5 |
常见原因
按命中率从高到低排序。
1. AI 猜了它没读过的类型签名
最常见:调用第三方库时,AI 凭记忆里旧版本的签名写代码,但你装的是新版本,API 已经改了。
// AI 写的(旧 stripe SDK)
const charge = await stripe.charges.create({ amount: 1000, currency: "usd" });
// 新 SDK(PaymentIntents 时代)
// → TS2554: Expected 0 arguments, but got 1
如何判断:错误码是 TS2554(参数数量错) / TS2345(参数类型错) / TS2339(属性不存在),且发生在第三方库调用点。
2. 强转 any / as unknown as T 掩盖真错误
AI 跑不通就 as any 或 as unknown as Foo 一刀切,编译过了但运行时直接 crash。
const data = (response as any).user.email; // response 其实是 { error: string }
// 编译通过,运行时 TypeError: Cannot read property 'email' of undefined
如何判断:grep -rn "as any\|as unknown as" src/ 数一下出现次数;AI 写的代码段里 > 2 处基本都是回避问题。
正确改法:用类型守卫去校验值,而不是断言。当你确实只是想确认某个对象符合某类型、又不想丢掉它推断出来的精确形状时,用 satisfies 运算符(TypeScript 4.9+)而不是 as——as 允许你对编译器撒谎,satisfies 不会。
// 别这样:as any 把真实错误压住了
const config = json as any;
// 更好:satisfies 校验形状,同时保留精确推断
const config = { port: 3000, host: "localhost" } satisfies ServerConfig;
3. 泛型参数漏写或写错
Array.prototype.reduce、useState、useRef、Map / Set、自定义 generic function——AI 经常默认 TypeScript 能推出来,但实际推成 never[] 或 unknown。
// AI 写的
const items = [].reduce((acc, x) => {
acc.push(x.name); // TS2339: Property 'push' does not exist on type 'never'
return acc;
}, []);
// 正确
const items = [] as string[];
// 或
const items = [].reduce<string[]>((acc, x) => { acc.push(x.name); return acc; }, []);
如何判断:错误信息里出现 never / unknown / Argument of type 'X' is not assignable to parameter of type 'never'。
4. strict 模式下没处理 null / undefined
AI 训练里大量旧代码没开 strictNullChecks,结果生成的代码假设字段一定存在。开了 strict 之后 TS2532: Object is possibly 'undefined' 满天飞。
function getEmail(user: User | null) {
return user.email; // TS18047: 'user' is possibly 'null'
}
如何判断:错误码 TS2531 / TS2532 / TS18047 / TS18048,且 tsconfig.json 里 "strict": true。
5. import 类型与值混用 / 缺 import type
verbatimModuleSyntax 或 isolatedModules 开启后,type-only import 必须用 import type,AI 经常忘。现在这点更容易踩到——verbatimModuleSyntax 已经是现代工具链(Vite、ts-node、Node 原生 TS 剥离)的推荐默认值,而强制只用可剥离语法的 erasableSyntaxOnly 也是和它配套使用的。
import { User } from "./types"; // 如果 User 只是 type,TS1484 / runtime "undefined"
// 正确
import type { User } from "./types";
如何判断:错误码 TS1484: 'User' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled 或运行时 User is undefined。要全仓批量修,社区 CLI(privatenumber)会自动改写这些 import:先 npx fix-verbatim-module-syntax --dry ./tsconfig.json 预览,确认无误再去掉 --dry 实际写入。
最短修复路径
按收益排序。Step 1+2+3 是 AI 类型错误的标准修复闭环。
Step 1:跑 tsc --noEmit 把所有错一次性暴露
不要看到第一个错就让 AI 改。一次性输出全部,让 AI 看见全貌:
npx tsc --noEmit --pretty false > ts-errors.txt
wc -l ts-errors.txt
--pretty false 会去掉颜色码和源码片段框,每行都是干净的 file:line:col - error TSxxxx: message 格式,贴进 prompt 最省事。如果你怀疑缓存把已经修好的错又报了一遍(TypeScript 只在开了 incremental 或 composite 时才写 .tsbuildinfo),就强制重新算一遍:删掉那个缓存文件;用了 project references 的话,跑 npx tsc --build --clean 再 npx tsc --build。
Step 2:把原文错误(含文件名:行号)整段贴回 AI
不要复述、不要总结。原文越完整,AI 修得越准:
我跑 tsc --noEmit 得到以下错误,请按以下要求修:
1. 不准用 as any / as unknown as
2. 不准为了通过加 // @ts-ignore / // @ts-expect-error
3. 如果某个第三方库的类型确实有问题,告诉我该升级哪个版本或装哪个 @types/*
4. 修完贴一段 diff,并解释每个改动的原因
错误日志:
src/api/user.ts:34:7 - error TS2322: Type 'string' is not assignable to type 'number'.
src/api/user.ts:41:12 - error TS2339: Property 'email' does not exist on type 'unknown'.
[完整粘贴]
Step 3:让 AI 在循环里自我验证(agent loop)
Claude Code、Cursor、Aider 都能跑 shell 命令,所以别一条条手喂错误——让工具自己把闭环跑完。好用的 prompt:
你要在本任务里:
1. 修复类型错误
2. 每次改完执行 `npx tsc --noEmit`
3. 如果还有错,继续修,最多 5 轮
4. 5 轮后还修不完,停下来告诉我哪些错你不会修,列出原文
按工具分(截至 2026 年 6 月):
- Aider ——
aider --auto-test --test-cmd "npx tsc --noEmit"。每次改完 Aider 都会跑这条命令,只要退出码非零就把输出喂回去并自动给出修复。 - Cursor —— 给安全命令打开 Agent 自动运行,让它能自己跑
tsc;或者配一个 Cursor Hook(Cursor 1.7+):在.cursor/hooks.json里注册一个afterFileEdit事件,指向.cursor/hooks/下的脚本,每次改完文件就跑tsc --noEmit并把错误回传给 agent。 - Claude Code —— 直接把上面那个循环告诉它就行,它会在集成终端里于两次编辑之间跑
tsc。把类型检查放进 pre-commit hook,这个循环就绕不过去了。
Step 4:第三方库类型不对就找 @types/* 或上游 .d.ts
很多 npm 包类型由社区维护:
npm install -D @types/node @types/react @types/lodash
# 看看库自己有没有打包类型
npm view <pkg> types
# 没 @types 也没自带就让 AI 写个最小 declaration
echo 'declare module "untyped-lib";' > src/types/untyped-lib.d.ts
避免 as any——临时声明 module 至少未来可逐步补全。
Step 5:常见错误码 → 修复方式对照表
| 错误码 | 含义 | 修复方向 |
|---|---|---|
| TS2322 | 类型不匹配 | 检查赋值两端类型,加显式 type 注解 |
| TS2339 | 属性不存在 | type 太宽,需要类型守卫 / in 检查 / 断言 |
| TS2345 | 参数类型不对 | 看函数签名,确认调用方传值类型 |
| TS2554 | 参数数量不对 | 库版本变了,查 changelog |
| TS2531/2532 | null/undefined 未处理 | 加 if (x) 守卫 / x?.foo / x! |
| TS7006 | 隐式 any | 给参数显式类型 |
| TS2304 | 找不到名字 | import 漏了 / @types/* 没装 |
| TS1484 | type-only import | 改成 import type |
| TS2741 | 缺必需属性 | 检查对象字面量,补齐字段 |
怎么确认真的修好了
“零错误”不等于”修好了”。三条都跑:
# 1. 干净的类型检查无输出,退出码 0
npx tsc --noEmit --pretty false ; echo "exit: $?"
# 2. 没有偷偷塞进新的逃生口
grep -rn "as any\|as unknown as\|@ts-ignore\|@ts-expect-error" src/
# 3. 代码真的能跑(类型过 != 逻辑对)
npm test # 或能跑到改动路径的最小脚本
如果第 1 步干净、但第 2 步搜出新的 as any / @ts-ignore,说明 AI 是把错误压住了而不是真修——打回去重新让它改。如果你是故意要抑制某处,带一行原因的 @ts-expect-error 远比 @ts-ignore 好:等底层类型修好、抑制不再需要的那天它会主动报错提醒你。
预防建议
- 把
tsc --noEmit放进 AI 的 agent loop / pre-commit hook,类型不过直接拦住 CLAUDE.md/.cursorrules写死:禁止as any、禁止@ts-ignore、禁止@ts-expect-error不带原因注释tsconfig.json开"strict": true+"noUncheckedIndexedAccess": true,让 AI 必须显式处理 null- code review 时搜
as any/// @ts-,零容忍 - 升级第三方依赖时让 AI 同步检查
.d.ts是否需要换@types/*版本 - 使用
tsc --noEmit --extendedDiagnostics分析 type 性能,避免 AI 写出递归 union 拖垮编译
常见问题
为什么我已经说了别用,AI 还是一直加 as any?
因为断言总能让编译器闭嘴,而模型在优化”build 变绿”。真正管用的是结构性约束,不是客气话:把禁令写进 CLAUDE.md / .cursorrules,再在 pre-commit hook 里加一道 grep 闸门(grep -rn "as any" src/ && exit 1),让 commit 直接失败。一条工具绕不过的规则,胜过一个它可以无视的请求。
编辑器里没报错,tsc 却报,为什么对不上?
编辑器的 language server 和命令行 tsc 可能跑的是不同的 TypeScript 版本,或不同的 tsconfig.json(monorepo 或一个 tsconfig.build.json 是常见元凶)。以项目根目录下的 tsc --noEmit 为准,并把工作区的 TypeScript 版本钉死,让编辑器用同一个。
// @ts-expect-error 算不算可以接受的修法?
有时算——它严格优于 @ts-ignore,因为底层问题一旦消失它也会报错,不会悄悄烂掉。前提是带一行说明原因的注释,且只在类型确实无法表达时用(比如上游某个已知坏掉的 .d.ts),而不是用来躲一个真 bug。
库的类型就是错的,怎么办?
先找替代修法:npm view <pkg> types 看库自己带不带类型,社区维护的话就装 @types/<pkg>。两者都没有,就在 .d.ts 里写个最小的 declare module 占位、之后逐步补全——比 as any 安全得多,因为它有作用域、还能后续收紧。
下次 AI 改代码怎么避免再犯?
把类型检查做成强制项,而不是靠你记得跑。tsc --noEmit 放进 agent loop、pre-commit hook 和 CI 三处;tsconfig.json 里开 "strict": true + "noUncheckedIndexedAccess": true,编译器会逼着 AI 一开始就处理 null 和索引访问。