Refactoring Legacy Code with AI Assistance

Legacy code is the code that works, makes money, and nobody wants to touch. Every team has it. That jQuery spaghetti from 2015. The class components nobody’s converted to hooks. The JavaScript modules that should have been TypeScript from the start. Refactoring it is important, risky, and historically slow.
AI changes the risk/reward calculation for legacy refactoring significantly. Here’s how I approach it.
The Legacy Code Challenge
The hardest part of refactoring legacy code isn’t the refactoring itself. It’s understanding what the code does well enough to not break it. Legacy code often has implicit behavior, undocumented side effects, and patterns that exist for reasons nobody remembers. Changing it without understanding it is how you create production incidents.
AI is surprisingly good at reading legacy code and explaining what it does. It’s seen every jQuery plugin, every Angular.js pattern, every callback-hell structure. It can tell you what the code does, even when the code can’t tell you itself.
Step 1: Always Write Tests First
This is non-negotiable. Before touching any legacy code, I have AI generate tests for the existing behavior. Not tests for what the code should do. Tests for what it actually does, including the quirks.
Here's a legacy JavaScript module that handles form validation.
Write tests that capture its CURRENT behavior, including any
quirks or unexpected behavior you notice. I need these tests
as a safety net before refactoring.
[paste the legacy code]
AI identifies behaviors you might miss: “This function returns undefined instead of false when the email field is empty, which matters because the calling code uses loose equality.” Those are the details that break things during refactoring.
Step 2: Converting jQuery to Modern JavaScript
I recently converted a 400-line jQuery form handler to vanilla JavaScript with modern patterns. Here’s how the AI-assisted process worked.
I didn’t ask AI to convert the entire file at once. That produces output that’s too large to review effectively. Instead, I went function by function:
Convert this jQuery function to vanilla JS. Preserve the exact
behavior. Use modern syntax (const/let, arrow functions,
querySelector). Don't add features or change logic.
// Legacy jQuery
function initDropdown() {
var $dropdown = $('.dropdown-toggle');
$dropdown.on('click', function(e) {
e.preventDefault();
var $menu = $(this).next('.dropdown-menu');
$('.dropdown-menu').not($menu).slideUp(200);
$menu.slideToggle(200);
$(this).toggleClass('active');
});
$(document).on('click', function(e) {
if (!$(e.target).closest('.dropdown').length) {
$('.dropdown-menu').slideUp(200);
$('.dropdown-toggle').removeClass('active');
}
});
}
The AI produced:
function initDropdown() {
const toggles = document.querySelectorAll('.dropdown-toggle');
toggles.forEach((toggle) => {
toggle.addEventListener('click', (e) => {
e.preventDefault();
const menu = toggle.nextElementSibling;
if (!menu?.classList.contains('dropdown-menu')) return;
// Close other menus
document.querySelectorAll('.dropdown-menu').forEach((other) => {
if (other !== menu) {
other.style.display = 'none';
}
});
// Toggle current menu
const isVisible = menu.style.display === 'block';
menu.style.display = isVisible ? 'none' : 'block';
toggle.classList.toggle('active');
});
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown-menu').forEach((menu) => {
menu.style.display = 'none';
});
toggles.forEach((t) => t.classList.remove('active'));
}
});
}
Note what the AI flagged: the jQuery slideToggle animation was replaced with simple show/hide. It mentioned this in its response and suggested adding CSS transitions if the animation was important. That’s the kind of behavioral difference that matters.
Step 3: Class Components to Hooks
Converting React class components to functional components with hooks is one of the most common refactoring tasks, and AI handles it well. The key prompt addition is specifying how to handle lifecycle methods:
Convert this class component to a functional component with hooks.
- componentDidMount → useEffect with empty deps
- componentDidUpdate → useEffect with specific deps
- componentWillUnmount → useEffect cleanup
- this.state → useState
- Class methods → useCallback where used as props
- Keep the same prop interface
[paste class component]
AI correctly handles the tricky cases: multiple state variables that were in a single this.state, lifecycle methods that combine mount and update logic, and this binding patterns that disappear with hooks.
Step 4: Adding TypeScript to JavaScript
Gradually typing a JavaScript codebase is a multi-month effort. AI accelerates it dramatically:
Add TypeScript types to this JavaScript module.
Rules:
- No 'any' types (use 'unknown' if truly unknown)
- Infer types from usage, don't guess
- Flag places where the current code has type ambiguity
- Use strict mode compatible types
[paste JS module]
The AI adds types and, critically, flags where the existing code has type issues. A function that sometimes returns a string and sometimes returns null without any indication in the signature. A parameter that’s documented as a number but is sometimes passed as a string. These are real bugs that TypeScript migration reveals, and AI spots them reliably.
The Safety Net Approach
My overall refactoring workflow with AI is:
- Tests first. Always. AI generates them from the existing code.
- Small changes. One function at a time, not entire files.
- Run tests after every change. If something breaks, you know exactly which conversion caused it.
- AI explains, you decide. Let AI tell you what the legacy code does. You decide what the refactored version should do.
- Review everything. AI-generated refactors are usually correct, but “usually” isn’t good enough for production code.
Legacy code refactoring used to be something teams avoided until it became an emergency. With AI, it’s manageable enough to do incrementally, a few functions per sprint, steadily improving the codebase without the risk of a big-bang rewrite. That’s how legacy code actually gets better: one safe, tested change at a time.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.
Discussion (0)
Sign in to join the discussion
No comments yet. Be the first to share your thoughts.
Related Articles

Building and Deploying Full-Stack Apps with AI Assistance
A weekend project walkthrough: building a full-stack task manager from architecture planning to deployment, with AI as t

AI-Assisted Database Design and Query Optimization
How to use AI for schema design, index recommendations, N+1 detection, and query optimization in PostgreSQL and MySQL.

Automating Repetitive Tasks with AI Scripts
Practical patterns for using AI to generate automation scripts for data migration, file processing, and scheduled tasks.