~/guides/refactor-legacy-ts-with-ai-14-call-sites-no-re-export-trap
§ GUIDE · APR 23, 2026 ADVANCED ADVANCED · LEGACY · REFACTOR v1.0

Refactor legacy TS with AI: 14 call sites, no re-export trap

A step-by-step AI refactor on a 63k-line TypeScript monorepo. The prompt, the re-export trap, and why Claude Opus 4.7 + Aider beat the IDE agent by 3 call sites.
Ryan CallowayStaff contributor
Peer score 8.4 10 min read

I keep a short checklist for when AI refactoring is worth using on a legacy TypeScript codebase and when to drop back to a codemod. The recurring r/typescript “AI rename broke our barrel re-exports silently” thread surfaces the same diagnosis every few weeks: the barrel-file export * path is the failure mode, the fix is to wire tsc --noEmit into the agent loop, and the re-export rule has to be explicit in the prompt. But that is one specific task inside a larger workflow. This guide covers the full sequence: how to map a legacy TypeScript codebase before touching a file, how to generate characterization tests that actually catch regressions, how to run targeted refactors with a type-checked loop, and when to hand off to ts-morph.

The core challenge: what makes legacy TypeScript hard to refactor safely

TypeScript treats import type and import differently at the erasure stage. A tool that only edits .ts files by pattern will leave a barrel re-export pointing at the old name, producing a type error on tsc --noEmit but a silent runtime success (because type-only imports erase). The error surface is deceptively narrow: 1 file broken, 27 downstream consumers fine, CI green on unit tests. The break is invisible until the next refactor lands on the same barrel and the error finally cascades.

Legacy codebases compound this with mixed patterns: some files use export * from, some name exports explicitly, some re-export under a namespace alias. No single grep or pattern-match covers all three. This is the task TCC editorial uses to separate “good enough for a demo” AI tooling from “good enough to merge on Friday.”

The workflow

Step 1: map the codebase in read-only mode before touching anything

Resist the urge to start changing code. Use Cursor’s Ask mode (read-only) or Claude Code with read-only flags to build a mental model first. The prompt below is the one I use on an unfamiliar TypeScript repo:

@src

Analyze this TypeScript codebase and give me:

1. A dependency graph of the top 10 most-imported modules
2. The 5 largest files by line count and what they do
3. Every place error handling is missing or inadequate (bare catch blocks, swallowed errors)
4. All global state or singletons that will make testing hard
5. Every barrel file using `export *` that could hide a rename target
6. Circular imports that would break a refactor if touched out of order

Format as a structured report I can reference during refactoring.

Save the output. You will reference it repeatedly. AI can read thousands of lines of unfamiliar code in seconds and surface hidden coupling that would take a human days to map. This is where the tool earns its keep, not in the mechanical edits.

Step 2: generate characterization tests before refactoring

Before refactoring a single line, capture the existing behavior in tests. These are not tests for “correct” behavior. They are characterization tests that document what the code actually does, bugs and all. If the refactoring changes a test result, you changed behavior.

@src/controllers/orders.ts @src/models/order.ts

Generate integration tests that capture the exact current behavior of the order processing flow:

1. Set up Jest with supertest for HTTP testing
2. Create test fixtures with realistic seed data
3. Write tests for every HTTP status code this endpoint can return
4. Include edge cases: negative quantities, empty cart, duplicate items
5. Test the actual error messages returned, not just status codes
6. Do NOT fix bugs in the tests. If the code returns a wrong status code, the test
   should expect that wrong code. Fixing bugs comes after the refactor.

Run the tests and verify they pass against the current code before we touch anything.

The explicit “do not fix bugs” instruction matters. AI tools silently “improve” behavior during test generation. You want tests that pass against the existing code. Regressions are changes from current behavior.

Step 3: extract the service layer before renaming anything

Most legacy TypeScript codebases mix business logic into route handlers. Separate that first, before any renaming or type tightening. Once the business logic lives in a service layer, it is testable in isolation and safe to rename types against.

@src/controllers/orders.ts @src/models/order.ts

Extract business logic from the orders controller into a new service:

1. Create src/services/order-service.ts
2. Move all business logic (validation, price calculation, database queries) into the service
3. Keep the controller as a thin HTTP adapter: parse request, call service, format response
4. The service accepts plain objects, not Express req/res objects
5. The service throws typed errors (ValidationError, NotFoundError) instead of calling res.status()
6. The controller catches service errors and maps them to HTTP responses
7. Update all imports
8. Run existing characterization tests after extraction to verify behavior is unchanged

Step 4: rename types with a type-checked loop

The TCC editorial refactor fixture (rename a TypeScript type across a 63k-line monorepo with 14 direct call sites and 4 barrel-file re-exports, clean working tree, Node 22, TypeScript 5.7, pnpm workspaces) measures the gap between tools. Claude Opus 4.7 in Aider architect mode hits 14 of 14 call sites including the 4 re-exports on the first pass. Cursor 3 with Composer 2 hits 11 of 14 in the median run and silently skips the re-exports on 3 of 5 runs.

The prompt that gets Claude to 14 of 14:

Rename the TypeScript type `UserProfile` to `AccountProfile` across this monorepo.

Rules, in order of priority:
1. Update the definition site first.
2. Update every direct `import { UserProfile }` to `import { AccountProfile }`.
3. Update every barrel file that re-exports `UserProfile` by name or through `export * from`.
4. Update every call site that references `UserProfile` as a value or a type.
5. Do not touch strings, comments, or documentation.
6. Run `pnpm -r tsc --noEmit` after every file edit. If it fails, roll back the last edit and retry.
7. When `pnpm -r tsc --noEmit` is green and every reference is renamed, print a list of files touched.

Rules 3 and 6 are the ones that matter. Without rule 3, every tool tested misses barrel re-exports. Without rule 6, every tool applies a correct-looking edit and leaves the type-checker broken.

Step 5: run Aider architect mode

aider --architect 
  --model claude-opus-4-7 
  --editor-model openai/gpt-5.3-codex 
  --auto-test 
  --test-cmd "pnpm -r tsc --noEmit" 
  packages/**/*.ts

Architect mode splits work between a planner (Claude Opus 4.7) and an editor (GPT-5.3-Codex, precise on diffs). The --auto-test flag makes Aider rerun tsc --noEmit after every edit and roll back on failure. On the TCC monorepo fixture, architect mode made 17 edits across 11 files, hit one tsc failure mid-run on a barrel re-export in the wrong order, rolled back, reordered the edits to touch the barrel first, and completed green on the second pass.

The Cursor 3 comparison

With the same prompt and the same tsc test command, Cursor 3 with Composer 2 made 14 of 17 correct edits in the median run. The 3 misses were barrel re-exports in packages/core/src/index.ts. The agent reported “rename complete” and did not surface the type errors from the re-export paths, because the IDE’s diagnostics panel checks the open editors, not the whole repo.

This is a defaults problem, not a bug. With Cursor configured to run an explicit test command after each edit, the score moves to 16 of 17. The remaining miss is a namespace-style re-export (export * as Profiles from "./profile") that the agent does not recognize as a rename target. The fix: add an explicit rule for namespace re-exports to the prompt.

Adding TypeScript incrementally

Converting a JavaScript codebase to TypeScript is a separate task from refactoring an existing TypeScript repo. The safe path is bottom-up with allowJs: true in tsconfig:

@src/models/order.js @src/services/order-service.ts

Convert src/models/order.js to TypeScript:

1. Rename to order.ts
2. Add interfaces for all data shapes (Order, OrderItem, OrderStatus)
3. Add proper types to all function parameters and return values
4. Use strict TypeScript: no `any`, enable strictNullChecks
5. Export the interfaces so the service layer can use them
6. Update imports in all files that reference this module
7. Keep tsconfig.json with allowJs: true so .js and .ts files coexist

Convert files bottom-up: models first, then services, then controllers, then routes. Each layer depends on the one below it. Typing the foundation first gives maximum value from the compiler’s type propagation.

Fixing the most dangerous legacy patterns

With characterization tests in place and a clean service layer, you can safely address the performance and correctness issues that accumulate in every legacy codebase:

@src/services @src/models

Find and fix the three worst database query patterns:

1. N+1 queries (loading related data in a loop instead of a join)
2. Missing WHERE clauses on UPDATE/DELETE statements
3. Queries that load entire tables when they only need specific columns

For each fix:
- Show the before/after SQL
- Explain the performance impact
- Add an index if the query plan benefits from one
- Create a migration file for any schema changes
- Do not change the data returned by any public function: only how it is fetched

The instruction “do not change the data returned” matters. Without it, the AI will fix the query and “improve” the return shape simultaneously, invalidating the characterization tests.

When to fall back to a codemod

On a bigger repo (250k+ lines) or a rename with ambiguous matches (a common word like “Config” appearing in 400 files), drop the AI loop and write a ts-morph codemod. AI is better at planning; ts-morph is better at bulk edits where correctness requires the AST and cannot tolerate a model deciding to “improve” adjacent code.

The hybrid pattern that works in practice:

For JavaScript-centric codemods, jscodeshift is still valid. TypeScript’s type space requires ts-morph when the transformation depends on type information (like tracking all re-exports of a specific type).

When AI breaks things

These are the four most common failure modes:

What the threads are saying

Multiple threads on r/typescript flagged the barrel-file re-export problem through the spring of 2026. The top community workaround is to lint against export * from in barrel files, which is the right long-term fix. On Hacker News, a thread about AI refactoring on large monorepos converged on “add an explicit test command to the agent loop.” That is rule 6 in the prompt above. The most useful answer on r/Aider about architect mode: use it any time the refactor touches more than 8 files. The planner step pays for itself in rollback avoidance alone.

Prompt library and scoring

The full monorepo-rename prompt with the tsc --noEmit guardrail is in the structured output library under refactor.rename.typed. The scoring rubric and run conditions are on the methodology page. The tool scores for this task class are on the Claude Opus 4.7 review and the Aider review.

Verdict

Start in read-only mode to map the codebase. Generate characterization tests before changing anything. Extract the service layer first. Then do targeted renames with Claude Opus 4.7 in Aider architect mode, pnpm -r tsc --noEmit wired into the loop, and the barrel re-export rule in the prompt explicitly. Convert TypeScript bottom-up with allowJs: true. On repos above 250k lines or with ambiguous rename targets, write the ts-morph codemod with Claude and review it before running it. Every other combination tested on the TCC fixture misses barrel files silently, and a silent miss on a rename is a type error that surfaces three sprints later.

esc