AI-Assisted Code Refactoring in Practice
Refactoring is the cure for technical debt but gets postponed because it's draining. AI makes untouchable legacy code touchable—but used wrong it introduces subtle bugs. This article shows the right way to do AI-assisted refactoring: pick scenarios, small steps with test guardrails, behavioral equivalence, and review priorities.
Key Points
- Topic Focus: Vibe Coding、TypeScript
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
1. Refactoring: the stone every developer is afraid to touch
Every legacy project has a few stretches of "ancestral code": no one can fully explain why it's written that way, but no one dares touch it. Fix one boundary condition, and it might explode in a late-night production alert three months later. So everyone tacitly walks around it, and the technical debt piles ever higher.
Refactoring is the cure for technical debt—every engineer knows that. What stands in the way isn't an understanding problem, it's a cost and risk problem: you first have to read the context, sort out every caller, confirm there are no missed side effects, then carefully edit, run tests, and edit again. For a function you could refactor in a workday, the up-front investigation alone might take two days. So refactoring is always bumped to "next sprint."
What AI truly changes isn't "is refactoring good," but the startup cost of refactoring. Let it read through the 2000-line legacy module for you, list every call site, and generate a "if I change this, what gets affected" impact analysis—work that no one wanted to do, now has a first draft in minutes. Once the bar comes down, a lot of "should-have-done-it" refactors actually get done.
But a lower bar doesn't mean lower risk. AI edits code fast and confidently, and confidence and correctness are two different things. Right under your nose it will write >= as >, and "optimize" away a branch that looked redundant but was actually a null fallback. Used right, it's an accelerator; used wrong, it's a bulk manufacturer of subtle bugs. This article is about taking only the speed, not the bugs.
2. Scenarios AI is good at: let it loose
One class of refactors shares these traits: clear goals, verifiable behavior, local context. Hand these to AI and it's fast and reliable.
1. Pure function extraction. A method mixes computation, IO, and formatting; splitting it into pure functions is the best fit for AI—because pure-function behavior is unit-testable and enumerable.
// Before refactoring: computation, side effects, and formatting all mashed together
async function checkout(order: Order) {
let total = 0;
for (const item of order.items) {
total += item.price * item.qty;
if (item.sku.startsWith('VIP-')) total -= item.price * 0.1;
}
total = Math.round(total * 100) / 100;
await db.save({ ...order, total });
sendEmail(order.userId, `Order amount ${total}`);
}
The instruction to AI is: "Extract the amount calculation into a pure function calcTotal(items), don't change any external behavior, and add a set of unit tests." The calcTotal it produces can be tested independently—and the discount logic previously buried inside checkout was untestable before.
2. API replacement / migration. Swap deprecated moment for dayjs, update old SDK method names, add a unified error-handling layer over fetch. For this kind of mechanical but error-prone refactor, AI is far more reliable than humans—a human scanning 50 call sites will definitely miss some.
3. Type completion. Pin any down to concrete types, add interfaces to function parameters. AI infers types from callers, then tsc acts as the gatekeeper, with high accuracy.
4. Naming unification / dead code removal / pattern unification. Where the same "user" is called user / account / profile, let it unify them; where the same validation logic is written in five different files, extract it into a util. Humans are too lazy for these scattered chores; AI does them happily.
3. The minefield AI shouldn't touch: human leads, AI assists at most
Another class of refactors has global context, hard-to-verify behavior, and high cost of error. In these places AI can only be an assistant; the decision must be human.
- Major architecture changes: split a monolith into microservices, change the state management scheme, redraw module boundaries. These decisions depend on business-evolution judgment; AI can't see the product plan six months out, and if you force a "refactor suggestion" out of it, it's usually textbook-correct but engineering-infeasible.
- Performance-sensitive areas: hot paths, serialization, caching layers. AI likes to turn "verbose but fast" code into "elegant but slow"—for example, replacing a
forloop with a chainedfilter().map()that walks the array twice; or turning an object cache into a closure variable, casually introducing a memory leak. - Concurrency / race logic: lock granularity, transaction boundaries, message ordering. These bugs are hard even to reproduce, let alone have AI fix by intuition. An
awaitthat looks redundant might be there to wait for the previous write to flush to disk.
My principle is: wherever "tests pass" is not enough to prove correctness, AI can only propose a plan, not land the code directly. It can help you list "the three possible risks of this refactor," but you're the one who has to make the call.
4. Safe refactoring methodology: small steps, test guardrails, behavioral equivalence
No matter who does the editing—human or AI—the underlying logic of safe refactoring is the same set of ideas Martin Fowler laid out twenty years ago and which still hold; AI just amplifies them.
Small steps: change one dimension at a time. Don't let AI "extract a function, rename, and swap the API" all in one change. One diff at a time, and every diff must independently pass tests. That way, if something goes wrong, git bisect pinpoints it. AI's tendency is "since I'm in here editing, let me do it all," and you have to pin it down with the instruction: "This change only does X; don't touch anything else."
Test guardrails: add tests first, then refactor. This is an iron rule. Don't let AI refactor code that has no test coverage—what would you use to prove it didn't break things? The correct order is: first have AI add tests (characterization tests, to lock in the current behavior), get them green, and only then start refactoring. When the tests are still green after the refactor, you have the confidence to say "the behavior didn't change."
Behavioral equivalence: refactoring is not optimization. The definition of refactoring is "improving internal structure without changing external behavior." If you want to also optimize performance, change a return value, or add a new parameter—stop; that's a different PR. Mixing them in makes review impossible and rollback impossible when something breaks.
5. The context you feed AI sets the floor of the output
The quality of AI refactoring is 70% determined by the context you feed it. Paste it a single function to refactor, and it will blindly mangle the callers.
A qualified refactoring prompt's context should include at least four pieces:
- The code to be refactored (complete, with imports)
- All callers (so it knows who a signature change affects)
- Existing tests (so it knows what the behavioral contract looks like)
- Explicit constraints (keep the API unchanged / don't change observable behavior / don't introduce new dependencies)
A reusable prompt skeleton:
I want to refactor <function/module>, with the goal of <extracting a pure function / swapping an API / adding types>.
Constraints:
- Keep the external API and return values exactly unchanged
- Do not change observable behavior
- Do not introduce new dependencies
- Do only this one thing; do not casually change anything else
Code to refactor: <paste>
Callers: <paste>
Existing tests: <paste>
First tell me how you plan to change it and which callers are affected; only proceed after I confirm.
That final "first tell me the plan" is critical—it forces AI to do an impact analysis before acting, so you can reject unworkable directions, saving a round trip versus reviewing after the fact.
6. Review priorities: watch the diff, not how pretty the code is
AI-generated code is deceptive: it reads very smoothly. But a pretty diff isn't a correct diff. During review, watch four things:
- Is the diff minimal? The ideal diff touches only what it should. If AI also reorders imports, rearranges unrelated functions, or unifies unrelated quote styles—send it back. The bigger the diff, the more bugs it hides.
- Did the behavior change? Focus on boundaries:
null/undefined/ empty arrays / 0 / negative numbers. AI loves to "clean up" defensive branches it doesn't understand—and those are often patches added after a predecessor stepped on a landmine. - Did it casually change unrelated things? AI will quietly turn
>=into>,||into??, and "optimize" names into what it thinks is better. Read line by line; these semantic micro-tweaks are the most dangerous. - Do the tests actually cover it? AI likes to write tests that "look like tests but only cover the happy path." Check: are boundary values tested? Are exception branches tested? If there were a bug before the refactor, would this test catch it?
One practical habit: have AI output a "list of risk points where behavior might change" alongside the refactor. The risks it lists itself you'll focus on; the ones it doesn't you'll catch by reading the diff line by line.
7. The complete workflow: from code smell to safe landing
String the above sections together and you get a workflow you can copy directly:
- Identify code smells—is this code a long function, duplicated logic, a sky full of
any, or calling a deprecated API? First judge whether it's the kind "suited to AI" (Section 2); don't force it if it isn't. - Add tests (guardrails)—have AI first add characterization tests to lock in the current behavior. Get them green before going further. No tests, no refactoring; that's the floor.
- Let AI refactor—feed it the four pieces of context (Section 5), require it to "produce a plan before acting," and change only one dimension at a time.
- Run the tests—green, move on; red, have it fix; if it can't fix,
git restoreand don't spin in place with it. - Review the diff—watch for minimality, behavioral equivalence, semantic micro-tweaks, and real test coverage (Section 6).
From 2 to 5, each step is a "safety valve." Skip any one of them, and AI's speed advantage gets eaten by the later cost of tracking down bugs.
Summary
- AI lowers the startup cost of refactoring, not the risk—it takes on the drudge work of reading code, finding callers, and listing impact, but the judgment of right vs. wrong edits is still yours.
- Picking the right scenario is the first step: pure-function extraction, API replacement, type completion, and naming/pattern unification are good to hand to AI; major architecture, performance-sensitive, and concurrency logic must be human-led.
- Small steps + test guardrails + behavioral equivalence form the triangle of safe refactoring; miss any one and you flip. Adding tests before refactoring is the iron rule.
- Context sets the floor: code to refactor + callers + tests + explicit constraints—feed all four and quality stabilizes.
- Review watches the diff, not the prettiness: minimality, boundaries, semantic micro-tweaks, real test coverage—the smoother AI writes, the more you must read line by line.
Legacy code isn't untouchable; it was just too costly to touch before. AI makes "affordable to touch" the default—but between "affordable to touch" and "touched correctly" lies a layer of engineering discipline with tests, guardrails, and your line-by-line review.
Related
Related Content
Article
What Is Vibe Coding? Will It Change How Programmers Work?
Technical trends, engineering practices, and an indie developer perspective on Vibe Coding, AI, and independent development.
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
Security Risks of AI Code Generation: Don't Blindly Trust Vibe Coding
Around Vibe Coding and AI: technology trends, engineering practices, and an indie developer's perspective.