Cursor / Claude Code / Copilot writes a chunk of TypeScript, you commit, then tsc --noEmit spits out a wall of TS2322: Type 'string' is not assignable to type 'number' or TS2339: Property 'foo' does not exist on type 'unknown'. LLMs have no runtime type reflection — they guess the shape of an interface from training data, which is fragile around generics, union types, and third-party .d.ts files.
Fastest fix: dump the complete error list with npx tsc --noEmit --pretty false > ts-errors.txt, paste it back verbatim with the rule “no as any, no @ts-ignore”, and make the agent re-run tsc after every edit until the count hits zero. The rest of this page breaks down the 5 high-frequency root causes, gives that self-verifying agent loop in full, and ends with an error-code-to-fix table.
Which bucket are you in?
Read the first error code in your log and jump straight to the cause:
| First error you see | Most likely cause | Section |
|---|---|---|
TS2554 / TS2345 on a library call | AI used an outdated API signature | Cause 1 |
| Compiles clean but crashes at runtime | An as any / as unknown as is hiding it | Cause 2 |
never / unknown in the message | Missing generic parameter | Cause 3 |
TS2531 / TS2532 / TS18047 | strict null not handled | Cause 4 |
TS1484 or runtime “X is undefined” | type/value import mixed up | Cause 5 |
Common causes
Ordered by hit rate, highest first.
1. AI guessed at a signature it hadn’t read
The most common failure: when calling a third-party library, the AI writes code matching a remembered older version, but you installed the new one and the API has changed.
// AI wrote (old Stripe SDK)
const charge = await stripe.charges.create({ amount: 1000, currency: "usd" });
// New SDK uses PaymentIntents
// → TS2554: Expected 0 arguments, but got 1
How to spot it: error codes are TS2554 (wrong arg count) / TS2345 (wrong arg type) / TS2339 (property missing), and the offending line is a third-party call.
2. Cast to any / as unknown as T hides the real error
When the AI can’t make types fit, it slaps as any or as unknown as Foo on. The compiler is happy; the runtime crashes.
const data = (response as any).user.email; // response is actually { error: string }
// Compiles fine, runtime: TypeError: Cannot read property 'email' of undefined
How to spot it: grep -rn "as any\|as unknown as" src/ and count. More than 2 in an AI-authored block is almost always avoidance.
The right fix: validate the value with a type guard instead of asserting. When you genuinely need to check that an object conforms to a type without throwing away its inferred shape, use the satisfies operator (TypeScript 4.9+) rather than as — as lets you lie to the compiler, satisfies does not.
// Avoid: as any silences a real error
const config = json as any;
// Better: satisfies validates the shape and keeps precise inference
const config = { port: 3000, host: "localhost" } satisfies ServerConfig;
3. Missing or wrong generic parameters
Array.prototype.reduce, useState, useRef, Map / Set, custom generic functions — AI often assumes TypeScript will infer correctly, but inference lands on never[] or unknown.
// AI wrote
const items = [].reduce((acc, x) => {
acc.push(x.name); // TS2339: Property 'push' does not exist on type 'never'
return acc;
}, []);
// Correct
const items = [] as string[];
// or
const items = [].reduce<string[]>((acc, x) => { acc.push(x.name); return acc; }, []);
How to spot it: errors mention never / unknown / Argument of type 'X' is not assignable to parameter of type 'never'.
4. Strict mode null / undefined not handled
A lot of training data is from pre-strict-null code, so the AI assumes fields always exist. Turn on strictNullChecks and TS2532: Object is possibly 'undefined' is everywhere.
function getEmail(user: User | null) {
return user.email; // TS18047: 'user' is possibly 'null'
}
How to spot it: error codes TS2531 / TS2532 / TS18047 / TS18048 with "strict": true in tsconfig.json.
5. Mixing type and value imports / missing import type
With verbatimModuleSyntax or isolatedModules on, type-only imports must use import type. AI frequently forgets. This bites more now that verbatimModuleSyntax is the recommended default in modern setups (Vite, ts-node, Node’s native TS stripping), and erasableSyntaxOnly — the flag that enforces Node-strippable syntax — pairs with it.
import { User } from "./types"; // If User is type-only: TS1484 or runtime "undefined"
// Correct
import type { User } from "./types";
How to spot it: TS1484: 'User' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled or a runtime “User is undefined.” For a whole-repo sweep, the community CLI (privatenumber) auto-rewrites the offending imports: run npx fix-verbatim-module-syntax --dry ./tsconfig.json to preview, then drop the --dry to apply.
Shortest path to fix
Ordered by ROI. Steps 1+2+3 are the standard fix loop for AI-generated type errors.
Step 1: Run tsc --noEmit to surface every error at once
Don’t re-prompt the AI on the first error you see. Get the whole list first so the AI sees the full picture:
npx tsc --noEmit --pretty false > ts-errors.txt
wc -l ts-errors.txt
--pretty false strips the color codes and the source-snippet framing, so every line is the plain file:line:col - error TSxxxx: message form that pastes cleanly into a prompt. If you suspect a stale cache is reporting errors you already fixed (TypeScript only writes .tsbuildinfo when incremental or composite is on), force a clean check: delete the cache file, or for project references run npx tsc --build --clean then npx tsc --build.
Step 2: Paste the verbatim errors (with file:line) back to the AI
Don’t paraphrase or summarize. The more complete the original, the more accurate the fix:
I ran tsc --noEmit and got the errors below. Please:
1. Do NOT use `as any` or `as unknown as`
2. Do NOT add // @ts-ignore / // @ts-expect-error to silence anything
3. If a third-party lib's types are broken, tell me which version to upgrade
or which @types/* to install
4. Reply with a diff and explain each change
Error log:
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'.
[paste in full]
Step 3: Put the AI in a self-verifying loop
Claude Code, Cursor, and Aider can all run shell commands, so don’t hand-feed errors one at a time — make the tool close the loop itself. The prompt that works:
For this task you must:
1. Fix the type errors
2. After each change, run `npx tsc --noEmit`
3. If errors remain, keep going. Max 5 iterations.
4. After 5, stop and tell me which errors you couldn't fix; paste them verbatim.
Per tool, as of June 2026:
- Aider —
aider --auto-test --test-cmd "npx tsc --noEmit". After every edit Aider runs the command, and if it exits non-zero it feeds the output back and proposes a fix automatically. - Cursor — turn on Agent auto-run for safe commands so it can run
tscitself, or wire a Cursor Hook (Cursor 1.7+): register anafterFileEdithook in.cursor/hooks.jsonpointing at a script in.cursor/hooks/that runstsc --noEmitand surfaces the errors back to the agent after each edit. - Claude Code — just tell it the loop above; it runs
tscin the integrated terminal between edits. Putting the type-check in a pre-commit hook makes the loop unavoidable.
Step 4: Wrong third-party types → find @types/* or upstream .d.ts
Many npm packages get community-maintained types:
npm install -D @types/node @types/react @types/lodash
# Does the library ship its own types?
npm view <pkg> types
# No @types and no bundled types → have the AI write a minimal declaration
echo 'declare module "untyped-lib";' > src/types/untyped-lib.d.ts
This beats as any because a module declaration can be filled in incrementally.
Step 5: Common TS error code → fix direction
| Code | Meaning | Fix direction |
|---|---|---|
| TS2322 | Type mismatch | Check both sides of the assignment; add an explicit type annotation |
| TS2339 | Property missing | Type is too wide; add a type guard / in check / assertion |
| TS2345 | Wrong arg type | Inspect the signature; fix the caller’s value |
| TS2554 | Wrong arg count | Library version changed; check the changelog |
| TS2531/2532 | null/undefined unhandled | Add if (x) guard / x?.foo / x! |
| TS7006 | Implicit any | Add an explicit parameter type |
| TS2304 | Cannot find name | Missing import / missing @types/* |
| TS1484 | Type-only import required | Change to import type |
| TS2741 | Missing required property | Inspect the object literal; add the field |
How to confirm it’s actually fixed
“Zero errors” is not the same as “fixed.” Run all three:
# 1. Clean type-check returns nothing and exit code 0
npx tsc --noEmit --pretty false ; echo "exit: $?"
# 2. No new escape hatches snuck in
grep -rn "as any\|as unknown as\|@ts-ignore\|@ts-expect-error" src/
# 3. The code actually runs (types passing != logic correct)
npm test # or the smallest script that exercises the changed path
If step 1 is clean but step 2 finds fresh as any / @ts-ignore, the AI silenced the error instead of fixing it — reject and re-prompt. If you suppressed something deliberately, an @ts-expect-error with a one-line reason is far better than @ts-ignore, because it errors the day the underlying type gets fixed and the suppression becomes unnecessary.
Prevention
- Put
tsc --noEmitin the AI’s agent loop and in pre-commit hooks; failing types block the commit - In
CLAUDE.md/.cursorrules: banas any, ban@ts-ignore, ban@ts-expect-errorwithout a reason comment - Set
"strict": trueand"noUncheckedIndexedAccess": trueintsconfig.jsonto force null handling - In code review, grep for
as any/// @ts-with zero tolerance - When upgrading third-party deps, have the AI also re-check whether
@types/*needs a matching bump - Run
tsc --noEmit --extendedDiagnosticsoccasionally to catch AI-generated recursive unions that tank compile speed
FAQ
Why does the AI keep adding as any even after I tell it not to?
Because casting always satisfies the compiler and the model optimizes for a green build. The fix that sticks is structural, not polite: put the ban in CLAUDE.md / .cursorrules, and add a grep gate (grep -rn "as any" src/ && exit 1) to your pre-commit hook so the commit physically fails. A rule the tool can’t bypass beats a request it can ignore.
My editor shows no errors but tsc does. Why the mismatch?
The editor’s language server and your CLI tsc can run different TypeScript versions or different tsconfig.json files (a monorepo or a tsconfig.build.json is the usual culprit). Trust tsc --noEmit from the project root as the source of truth, and pin the workspace TypeScript version so the editor uses the same one.
Is // @ts-expect-error an acceptable fix?
Sometimes — it’s strictly better than @ts-ignore because it also errors once the underlying problem is gone, so it can’t rot silently. Only use it with a comment saying why, and only when the type genuinely can’t be expressed (a known-broken upstream .d.ts, for example), not to dodge a real bug.
The library types are just wrong. What do I do?
First check for a fix: npm view <pkg> types to see if it ships types, and install @types/<pkg> if the community maintains them. If both are missing, write a minimal declare module stub in a .d.ts and fill it in incrementally — far safer than as any because it’s scoped and you can tighten it later.
How do I stop this happening again on the next AI edit?
Make the type-check non-optional, not a habit you have to remember. Run tsc --noEmit in your agent loop, your pre-commit hook, and CI; with "strict": true and "noUncheckedIndexedAccess": true in tsconfig.json, the compiler forces the AI to handle null and indexing up front.
Related
- AI code broke build
- AI pre-commit review workflow
- Claude Code SEO audit
- AI dependency upgrade workflow
- AI tests pass but feature broken
Tags: #AI coding #Debug #Troubleshooting