Skip to content

NEXT.JS

Claude Code with TypeScript: What tsc Tells It

Claude Code with TypeScript found all three planted errors without running the compiler. Handing it `tsc --noEmit` did not make it more accurate — it made it stop reading files, for a third of the tokens.

The interesting thing about pointing an agent at a TypeScript project is that the project already contains a program whose entire job is to find the errors. So the question is not whether Claude Code can find a type error. It is whether tsc tells it anything it did not already know — and what asking costs.

We built a three-file project with three planted errors, one per compiler behaviour class, and ran nine billed arms against it on CLI 2.1.280. Every arm found every error. The compiler output changed nothing about the answer. What it changed was how much the agent read to produce it: the arms that were handed tsc --noEmit output opened no files at all and used a third of the input-side tokens.

Key takeaways

  • Three source-only arms found 3 of 3 errors with the shell denied, naming the right file, line and TSxxxx code each time — including the one only strict reports.
  • Three arms given the compiler output also found 3 of 3, and read nothing: 31,177 input-side tokens against 94,352–94,380 for the arms that had to look.
  • tsconfig.json was read unprompted on every arm that opened a file at all.
  • The unrestricted arm was the most expensive route to the same answer — it ran the compiler itself, took 7 turns and consumed 183,357 input-side tokens.
  • strict: true added exactly one error to the same three files: TS7006. The other two are reported either way.

The short answer

Run the compiler yourself and paste the output. On this project it did not improve what Claude Code found — six arms out of six found everything either way — but it replaced roughly 63,000 tokens of file reading with 297 bytes of text, and it was the cheapest of the three routes we measured. Do not skip strict because the agent seems to cope without it: the agent found the implicit-any regardless, but the compiler only reports it when strict is on, and the compiler is what your CI has.

For the wider setup this sits inside — where CLAUDE.md goes, what the permission system does — the Claude Code CLI overview and our complete Claude Code guide cover the foundation workflow this fixture is a miniature of.

What tsc already knows

The fixture is deliberately small: two source files, a tsconfig.json, and one empty directory. Three errors, chosen because the compiler treats each one differently.

scripts/fixtures/ts-project/tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "moduleResolution": "bundler",
    "paths": { "@/*": ["./*"] }
  },
  "include": ["**/*.ts"]
}
src/util.ts
// E1 — no annotation on `value`
export function slugify(value) {
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}

export function repeat(text: string, times: number): string {
  return new Array(times + 1).join(text);
}
src/main.ts
import { formatDate } from "@/lib/dates";   // E2 — `lib/dates.ts` does not exist
import { repeat, slugify } from "./util";

export function banner(title: string): string {
  const rule = repeat("-", "24");           // E3 — string where number is declared
  return [formatDate(new Date()), slugify(title), rule].join("\n");
}

Run the compiler and it says all three in 297 bytes:

tsc --noEmit, strict: true
src/main.ts(3,28): error TS2307: Cannot find module '@/lib/dates' or its corresponding type declarations.
src/main.ts(10,28): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
src/util.ts(4,25): error TS7006: Parameter 'value' implicitly has an 'any' type.

That is the baseline the agent is being compared against — not a benchmark, a ground truth. The compiler is right by construction here, because we planted the errors.

Does Claude Code read tsconfig.json?

Yes, and without being asked. Even though the TypeScript tsconfig documentation defines dozens of compiler options, the prompt given to every arm was the same and never mentions configuration:

The prompt, identical across arms
This is a small TypeScript project. List every type error in it. For each one
give the file, the line, and the TypeScript error code (TSxxxx). Do not fix
anything. Answer with a list and nothing else.

Every arm that opened a file at all opened tsconfig.json — three source-only runs and the unrestricted run, four for four. One of them volunteered the fact in its answer without being asked how it worked:

Arm A, run 1, closing line of the answer
I didn't run `tsc`. I have no shell tool here, so this list comes from reading
`tsconfig.json`, `src/util.ts` and `src/main.ts`.

This is the part that generalises least gracefully to a real repository. A three-file project is small enough to read exhaustively; what actually fills a context window is the measurement for what happens when it is not.

The three errors, and the one strict adds

The three planted errors are not interchangeable. Each one exercises a different part of the compiler (as documented in the TypeScript compiler options handbook), and only one of them depends on strict.

CodeClassReported with strict: false?
E1TS7006implicit anyNo
E2TS2307module resolutionYes
E3TS2345assignabilityYes

Same files, one flag, run live on every check:

Terminal
node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json
# → 3 errors: TS2307 TS2345 TS7006

node node_modules/typescript/bin/tsc --noEmit -p tsconfig.json --strict false
# → 2 errors: TS2307 TS2345

slugify is exactly as untyped in both runs. The only thing strict: false changes is whether anyone is told.

What the compiler output actually changed

Two arms, three runs each, shell denied in both so neither could run the compiler itself. Arm A got the source. Arm B got the source and the 297 bytes above.

RunA: foundA input-sideA toolsB: foundB input-sideB tools
13/394,354Glob, Read3/331,177none
23/394,380Glob, Read3/331,177none
33/394,352Glob, Read3/331,177none

Six runs, six times 3 of 3. Not once did the compiler output cause an error to be found that reading the source did not find, and not once did its absence cause one to be missed. On accuracy, against this fixture, tsc told Claude Code nothing it could not work out.

That is the null result, and it is the honest headline. It is also not the whole of it, because the right-hand column never stopped being different.

The context cost runs the other way

The expectation going in was that pasting compiler output costs context. It does the opposite here, and the reason is in the tools column: arm B read nothing, in all three runs.

  • Arm A: ~94,360 input-side tokens, two tool calls, then an answer.
  • Arm B: 31,177 input-side tokens — identical to the token in all three runs, because with no file reads the input is deterministic — and no tool calls.

That is 0.33x, a saving of roughly 63,000 input-side tokens, bought with 297 bytes. The output did not add context; it removed the reason to go looking for any. Output tokens moved the same way — 642–745 for arm A against 94–118 for arm B, which transcribed a list it had already been given.

The unrestricted run is the expensive one

The two arms above had the shell denied, which is not how anybody runs this. So the third arm ran with nothing denied and no compiler output supplied — the realistic default.

It did what you would expect: reached for the shell, ran the compiler, and reported the three errors. It took 7 turns and 183,357 input-side tokens.

ArmRoute to the answerInput-sideFound
Bhanded the output31,1773/3
Aread the source~94,3603/3
Cran the compiler itself183,3573/3

The most autonomous route was the most expensive, by 5.9x over the cheapest, for an identical answer. Letting the agent discover that it should run tsc, run it, and read the result costs more than running it yourself and pasting three lines — on a project with three files. The gap is unlikely to close as the project grows.

What strict: true does and does not do

Two things are worth separating, because the measurement only supports one of them.

Measured: strict: true changes what the compiler reports. Three errors instead of two on identical files, and TS7006 is the whole difference.

Not measured: whether strict: true changes the code Claude Code writes. Nothing in this experiment asked it to generate anything — every arm was told "do not fix anything" precisely so that the finding would be about detection rather than repair. A generation experiment is a different fixture and a different article.

What can be said is narrower and still useful: the agent identified the unannotated parameter in all six restricted runs, including the three where it had no compiler output at all. It did not need strict to see the problem. Your CI does — which is the argument for the flag that survives this result intact, and the one setting up CLAUDE.md is the right place to write down.

What we are not claiming

That this generalises to a real repository. Three files is small enough to read exhaustively; the site this fixture lives in has 77 .ts and .tsx files and tsc --noEmit output that is empty rather than 297 bytes. The 0.33x ratio is this project's, and the direction is more likely to hold than the magnitude.

That the agent cannot be fooled. The errors were planted, obvious and three in number. A subtle variance bug or a type that is wrong rather than missing is a different test, and this one says nothing about it.

That strict does not affect generated code. It was not tested. See above.

That nine arms is a lot. It is three runs of two conditions plus one of a third, on one machine, one account, one day, one model tier at --effort low. The A/B result reproduced three times for three, which is why it is stated as a finding; the arm C figure is a single observation and is stated as one.

That we know why arm B skipped reading. It did, in all three runs, and the token counts prove it. Whether that is a policy, a heuristic or a coincidence of this prompt is not visible from outside and is not claimed.

Common mistakes

  • Assuming the compiler output is the expensive part. It was 297 bytes and it saved roughly 63,000 tokens, because it removed the need to explore.
  • Letting the agent discover it should run tsc. It will, and on this fixture that route cost 5.9x the cheapest one for the same three errors.
  • Reading a green tsc --noEmit as "no type problems". With strict: false this project is two-thirds green and exactly as broken — see the table above.
  • Trusting a check that cannot fail. The first version of the script behind this article shelled out to npx on Windows, where it is a .cmd shim that execFileSync cannot launch; the ENOENT landed in the same catch as a normal non-zero tsc exit and the fixture recorded zero errors for a project with three. The guards caught it because they assert a count. A fixture that only records what it was given would have published the number.
  • Comparing a cold run to a warm one. Run 1 here cost 4x runs 2 and 3 on the same arm, and none of that difference is about TypeScript.

Conclusion

Run tsc --noEmit yourself and paste the output into the prompt. It will not make Claude Code more accurate — on this fixture nothing did, because it was already finding everything — but it stops the agent reading your repository to rediscover what the compiler computed in a second, and on three files that was the difference between 31,177 tokens and 183,357.

Then keep strict: true for the reason it always existed. The agent found the implicit any without it. The compiler will not, and the compiler is what runs in CI when nobody is watching — the same argument that applies to every other silent failure in a build, and the reason this site has a check for each one it has been bitten by.

We re-run this with an internal check script. The compiler arms re-run live on every invocation; the billed arms are replayed from the committed fixture unless we explicitly re-measure them.

Frequently asked questions

Does Claude Code read tsconfig.json on its own?
Yes, on every run here that opened a file at all. Across three source-only arms and one unrestricted arm on CLI 2.1.280, tsconfig.json was read without the prompt ever mentioning it, and one arm said so unprompted in its answer. The three arms that were handed the compiler output read nothing at all, so they had no occasion to — that is an absence of opportunity rather than a decision not to.
Does giving Claude Code tsc output help it find type errors?
Not on accuracy. Six arms — three with the compiler output and three without — each named all three planted errors with the correct file, line and TSxxxx code. What the output changed was effort: every arm that received it answered from the text and opened no files, using about a third of the input-side tokens.
How many tokens does tsc --noEmit output cost in context?
The output itself was 297 bytes for three errors. Measured through Claude Code's own usage reporting, the arms that received it consumed 31,177 input-side tokens against 94,352–94,380 for the arms that had to read the source — so supplying it was about 63,000 tokens cheaper, not more expensive. The saving is the file reads it replaced, not the output's own size.
Does strict: true change the code Claude Code writes?
This experiment did not test generation, so that question is open here. What it does show is what strict changes about reporting: the same three files produced three errors with strict: true and two with strict: false. The implicit-any error TS7006 is the only one the flag adds; the module-resolution and assignability errors are reported either way.
Which TypeScript errors does strict: true hide when it is off?
On this fixture, exactly one of three. With strict: false the compiler still reported TS2307 for an unresolved @/* import and TS2345 for a string passed where a number was declared, but TS7006 for an unannotated parameter disappeared. The function was equally wrong in both runs; only the report changed.
Should I let Claude Code run tsc itself?
It will if it can — the unrestricted arm reached for the shell, ran the compiler and took seven turns. It reached the same three-error answer as the restricted arms while consuming 183,357 input-side tokens, roughly twice the source-reading arm and nearly six times the arm that was simply handed the output. Running the command for it and pasting the result was the cheapest of the three routes here.

Muhammad Kashif

Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.