Claude Code migrated 200K lines of JavaScript to strict TypeScript in six weeks — here's the playbook

One Node.js team handed a 200,000-line monolith to Claude Code with a strict CLAUDE.md, leaf-first batching, and tight prompt patterns. The result: full strict TypeScript in six weeks, real null bugs caught in production, zero blanket any.

Most teams put off the TypeScript migration their codebase desperately needs because the manual cost is brutal. One engineering team running a six-year-old 200,000-line Node.js monolith — eight contributors, 200-plus `// @ts-ignore` headstones from a failed 2022 attempt that stalled at 15 percent — handed the entire conversion to Claude Code and shipped it in six weeks. The result is the clearest signal yet that agentic coding has crossed from autocomplete into durable refactoring at scale.

Why automated codemods kept failing

The obvious first move is a codemod — Airbnb's `ts-migrate` or `jscodeshift`. Both rename files and sprinkle `any` wherever a type cannot be inferred statically. That is not a migration. It is a legalized escape hatch in every function signature.

Static tools cannot read intent. Consider a price formatter called from fifteen places across products, checkout, and revenue reports. A codemod gives it `(value: any, currency: any)`. Claude reads every call site, observes that `value` is sometimes `null` and sometimes a `number`, and emits `(value: number | null | undefined, currency: string)`. The difference between formal conversion and understanding is the entire ballgame.

The 40 percent of effort that determines the outcome

Preparation is most of the work. Teams that skip it complain that Claude "puts `any` everywhere." It does not — there simply is no context to draw from.

Three things have to exist before the first file is converted. A migration-grade `tsconfig.json` with `allowJs: true` and `strict: false` so JS and TS coexist while `tsc --noEmit` runs in CI from day one. A `CLAUDE.md` with hard rules: convert one module at a time, prefer `unknown` over `any`, never refactor logic during type conversion, flag side-effecting code for manual review. And a `src/types/` directory containing boundary types — API responses, database models, shared interfaces — so every converted file has something concrete to lean on.

The "no logic changes" rule is the most important one. Without it, the agent will quietly "improve" code mid-conversion, and reviewers lose the ability to tell a real bug fix from a stylistic tweak in the diff.

Batch architecture: leaf modules first

Handing Claude the whole project is a proven path to failure. Context windows blow past 800 lines, types from the start of a file drift by the end, and `any` returns as the path of least resistance.

The team's rule was one module per pull request, one batch per module, five to fifteen files per batch. Sequencing matters more than batch size: `utils/` and `helpers/` first (no internal dependencies), then `services/`, then `controllers/` and `routes/`, then `core/` and `app.ts` last. A weekly scorecard script counting JS-versus-TS files and `--strict` errors went into Slack every Friday. Watching 12 percent climb to 45 percent to 78 percent did more for team motivation than any standup.

A migration PR should be boring. If a reviewer thinks the diff is interesting, the agent did something it was not supposed to.

The prompt patterns that survived production

Four prompt shapes carried the project.

**Context-loaded conversion**: name the file, list every call site by file and line, restate the CLAUDE.md rules, and end with "list any places where you had to make a judgment call." That last line is the difference between trust and blind acceptance — Claude will volunteer where it inferred a type from incomplete usage.

**Type discovery by reverse lookup**: "Find every place `getUserData()` is called. For each, show the file, the line, how the result is used, and what type that implies. Then suggest a return type." Works only because Claude has the codebase as context and can genuinely grep.

**Surgical regression recovery**: when the agent changes behavior, never say "you broke something." Say "on line 23 you replaced `!value` (which catches `null`, `undefined`, `0`, and empty string) with an explicit null check that misses `0` and empty string. Revert only that change. Keep all type annotations." Specificity is the difference between a fix and a rewrite.

**Batch error triage**: after a batch, ask Claude to walk through the resulting `tsc` errors and classify each as either a real type gap or an inference miss it can repair itself.

What this actually proves

The case is not that Claude Code writes perfect TypeScript. It is that a sufficiently constrained agent with a well-designed prompt grammar and a leaf-first batching strategy can do work that previously required either three months of feature freeze or two thousand engineer-hours of dedicated migration time. The agent ceiling is no longer set by the model — it is set by how much scaffolding the team puts around it before letting it touch code.

For any team sitting on a JavaScript codebase larger than 50,000 lines, the economic argument for "we cannot afford to migrate" just changed.

Source: Anthropic — Claude Code