From Scratch: A Complete Guide to Refactoring Legacy Code with AI
Learn how to safely refactor old code using AI tools, including the complete process of assessment, planning, execution, and verification.
The Pain of Legacy Code
Every programmer has encountered this kind of code:
// Written in 2018, no one dares touch it
function processData(d, f, t, opts) {
var r = [];
for (var i = 0; i < d.length; i++) {
if (d[i].type == t || t == null) {
var x = d[i];
if (opts && opts.filter) {
if (opts.filter(x)) {
if (f) {
r.push(f(x));
} else {
r.push(x);
}
}
} else {
if (f) {
r.push(f(x));
} else {
r.push(x);
}
}
}
}
return r;
}
Don't understand it? No worries, AI can help.
Pre-Refactoring Preparation
1. Assess Code Coverage
# Run tests and generate coverage report
npm test -- --coverage
If coverage is below 60%, add tests first.
2. Get AI to Explain Code
Prompt:
Analyze the following code's functionality:
1. Explain each parameter's purpose
2. Describe the function's main logic
3. List potential issues and risks
Code:
[Paste code]
AI Analysis:
This function filters and transforms data arrays:
Parameters:
- d: data array
- f: optional transform function
- t: optional type filter
- opts: config object with additional filter
Problems:
1. Parameter names too cryptic
2. Deeply nested if statements (4 levels)
3. Uses var instead of let/const
4. Duplicate code (push logic appears twice)
5. Missing error handling
3. Set Refactoring Goals
Based on AI analysis, set clear objectives:
- [ ] Improve naming
- [ ] Reduce nesting levels
- [ ] Use modern JavaScript syntax
- [ ] Add TypeScript types
- [ ] Eliminate code duplication
Step-by-Step Refactoring Strategy
Step 1: Add Tests First
Prompt:
Generate comprehensive unit tests for this function covering:
1. Normal usage scenarios
2. Edge cases
3. Null/undefined handling
4. Various parameter combinations
Step 2: Refactor in Small Steps
Rule: Change one thing at a time
Round 1: Improve naming
function processData(data, transform, typeFilter, options) {
var results = [];
for (var i = 0; i < data.length; i++) {
if (data[i].type == typeFilter || typeFilter == null) {
var item = data[i];
// ... rest unchanged
}
}
return results;
}
Run tests → All pass → Commit
Round 2: Modernize syntax
function processData(data, transform, typeFilter, options) {
const results = [];
for (const item of data) {
if (item.type === typeFilter || typeFilter === null) {
// ... rest unchanged
}
}
return results;
}
Run tests → All pass → Commit
Round 3: Reduce nesting
function processData(data, transform, typeFilter, options) {
const results = [];
for (const item of data) {
if (typeFilter !== null && item.type !== typeFilter) {
continue;
}
if (options?.filter && !options.filter(item)) {
continue;
}
const result = transform ? transform(item) : item;
results.push(result);
}
return results;
}
Step 3: Final AI-Assisted Optimization
Prompt:
Refactor this code to modern TypeScript with:
1. Complete type definitions
2. Functional programming style
3. JSDoc documentation
4. Identical functionality
Code:
[Paste previous version]
Final Version:
interface DataItem {
type: string;
[key: string]: unknown;
}
interface ProcessOptions<T extends DataItem> {
filter?: (item: T) => boolean;
}
/**
* Filter and transform data array
*
* @param data - Data array to process
* @param transform - Optional transform function
* @param typeFilter - Optional type filter, null means no filter
* @param options - Additional configuration
* @returns Processed data array
*/
export function processData<T extends DataItem, R = T>(
data: T[],
transform?: ((item: T) => R) | null,
typeFilter?: string | null,
options?: ProcessOptions<T>
): R[] {
return data
.filter(item => typeFilter === null || item.type === typeFilter)
.filter(item => !options?.filter || options.filter(item))
.map(item => (transform ? transform(item) : item) as R);
}
Refactoring Checklist
- [ ] All tests pass
- [ ] Coverage hasn't decreased
- [ ] No significant performance degradation
- [ ] Improved code readability
- [ ] Team review approval
Common Pitfalls
1. Changing Too Much at Once
❌ Wrong: Refactor entire file at once ✅ Right: Change one thing, commit frequently
2. Ignoring Tests
❌ Wrong: Refactor first, add tests later ✅ Right: Add tests first, then refactor
3. Blindly Trusting AI
❌ Wrong: Use whatever AI generates ✅ Right: Always run tests for verification
Summary
AI makes legacy code refactoring manageable:
- Understand: Let AI explain complex code
- Test: Let AI generate test cases
- Refactor: Let AI suggest improvements
- Verify: Always run tests
Remember: AI is an assistant, not a replacement. You have final say.
Further Reading: AI Coding Tips | Prompt Engineering Guide