Stockio

Refactor Code With AI: Legacy JavaScript in Safe Steps

Refactor legacy JavaScript through narrow AI-assisted patches, characterisation tests and boundary types, while preserving hidden contracts and a practical rollback path.

By Ravi Desai

DevOps & Security Engineer ·

On this page

Legacy JavaScript rarely fails because nobody knows a cleaner syntax. It fails because important behaviour lives in undocumented call sites, unusual data and historical workarounds. An AI assistant can rewrite a module neatly while removing the condition that keeps an old customer integration working.

Use AI to make changes that are small enough to explain and cheap enough to reverse. The objective is not a modern-looking file. It is a clearer implementation with evidence that supported behaviour still works, followed by separately approved changes to behaviour that should not survive.

Establish the behaviour you must preserve

Choose a narrow entry point: one formatter, one data adapter or one event handler. List its callers and the values they actually pass. Look for hidden contracts such as mutating an input object, preserving property order for a downstream consumer, or returning an empty string instead of null. These may be undesirable, but they are still dependencies until deliberately removed.

Ask the model to explain the function without rewriting it. Require references to the relevant branches and distinguish observed facts from guesses. Verify that explanation against code and examples. A fluent description can overlook a fall-through case just as easily as a developer can.

Create characterisation tests around representative production behaviour using sanitised fixtures. Include missing properties, empty collections and malformed-but-currently-accepted values. The article on AI-generated unit tests explains how to avoid copying implementation assumptions into every assertion. Label known bugs explicitly so preserving behaviour does not become permanent approval of those bugs.

Record the runtime, module format and build constraints. A transformation that works in a modern browser may break an older embedded browser, and replacing CommonJS exports can change how existing consumers load a module.

Separate discovery from editing

Give the assistant a read-only investigation task first. Ask it to find call sites, side effects, dependencies and existing tests, then propose a sequence of changes. Review the sequence before authorising any edits. This prevents a modest cleanup request from spreading into configuration, dependency and architecture changes.

Useful initial instructions are specific about scope:

Inspect normaliseOrder and its direct callers. Do not edit files.
List observable behaviour, side effects and uncertain assumptions.
Propose three independently testable refactoring steps.
Preserve exports, mutation behaviour and error handling.
Do not upgrade dependencies or change module format.

Choose assistants from the AI coding assistants directory based on repository visibility, patch review and permission controls. Broad context can improve discovery, but broad write access is not required. Exclude secrets and unrelated customer data from the context you share.

Set a change budget for each patch. A practical first step might rename local variables and extract one pure calculation, without touching public names or control flow. If the model finds a larger architectural problem, log it for later rather than accepting an unplanned rewrite.

Make one mechanical transformation at a time

Start with changes whose effects you can reason about locally: extracting duplicate literals, naming a predicate or isolating a formatter. Run the relevant checks after each step. Small patches make it easier to identify which transformation introduced an unexpected difference and to discard only that change.

Be cautious with semantic lookalikes. Replacing a truthiness check with a nullish default changes how zero, false and empty strings behave. Converting a loop to a map can alter mutation and allocation patterns. Arrow functions have different binding rules from ordinary functions. The MDN JavaScript guide is a better authority on those semantics than a confident generated explanation.

Preserve evaluation order when getters, callbacks or network requests can have side effects. A seemingly harmless extraction may call a getter twice or delay a mutation until after an observer runs. Ask the model to identify order-sensitive expressions, then inspect them yourself rather than treating the answer as a guarantee.

Use a single-purpose diff as the review unit. Do not mix formatting an entire file with changing its branches. If project tooling reformats touched lines, keep that noise contained and explain it in the review notes.

Introduce types at the boundary

Types can make a refactor safer when they describe real data rather than an idealised sample. Collect several sanitised payloads and compare them with the documented API contract. Separate fields that are absent from fields explicitly set to null, and retain string identifiers even when they contain only digits.

Use the JSON to TypeScript tool to turn representative fixtures into a first draft of boundary interfaces. Review the output for optional properties, unions and nested variants before adding it to the project. A generated interface describes the supplied examples; it cannot establish every value the server may send.

For a JavaScript codebase, incremental checking through JSDoc and targeted tooling may be less disruptive than renaming every file. The TypeScript migration guidance describes how gradual adoption works. Keep compiler configuration changes separate from the behaviour-preserving patch so reviewers can understand each independently.

Do not silence uncertainty with broad casts or any types just to obtain a clean check. Document unresolved inputs and narrow them explicitly where they enter the system. The guide to types from real API data provides a fuller workflow for separating observed shapes from validated contracts.

Verify equivalence and plan reversal

Run old and new implementations against the same fixture corpus where practical. Compare returned values, thrown errors and externally visible side effects. This differential testing is particularly useful for parsers and adapters, but it only establishes agreement on the exercised inputs. It does not prove that the original implementation was correct.

For asynchronous code, include ordering, rejection and cancellation scenarios. A function returning the same eventual value can still break callers if it now throws synchronously instead of rejecting a promise. Check subscription cleanup and event listener removal when extracting lifecycle code.

Inspect the final diff with tests disabled in your mind: can you explain why each changed expression preserves the contract? Use the Git diff documentation to review changes without allowing whitespace noise to dominate. Tests provide evidence; careful code review addresses gaps in that evidence.

Keep a rollback path proportional to risk. An internal utility may need only a focused revert. A widely used adapter may need staged rollout, error monitoring and a temporary switch between implementations. Define what signal would trigger reversal before deployment, not during an incident.

Write the completion note in terms of verified outcomes: which behaviour was preserved, which checks actually ran, and what uncertainty remains. Do not report “fully modernised” when you have safely extracted one function. Small, explicit improvements create a reliable foundation for the next patch.

Key takeaways

  • Discover callers and hidden behaviour before requesting edits.
  • Separate investigation, mechanical refactoring and intentional behaviour changes.
  • Keep each patch small enough to review and revert independently.
  • Use generated types as reviewed drafts, not proof of runtime inputs.
  • Combine regression tests, differential checks and a concrete rollback plan.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in Dev Tools & AI Coding
Dev Tools & AI Coding

10 Best AI Coding Assistants

Compare ten AI coding assistants for editor help, repository questions, and terminal workflows, with practical notes on strengths, trade-offs, privacy, and choosing a tool you can review.

Includes: JSON to TSSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Write SQL With AI: Generate and Optimize Queries Safely

Turn reporting questions into tested SQL with explicit data contracts, edge-case fixtures, execution-plan evidence, and safe application types before trusting an AI-generated query.

Includes: JSON to TSSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Cursor AI Tips: Give the AI the Right Codebase Context

Give Cursor focused repository context, trustworthy API examples, and clear edit boundaries so generated changes fit your architecture instead of inventing a parallel one.

Includes: JSON to TSGrace ChenGrace Chen
Dev Tools & AI Coding

AGENTS.md and Cursor Rules Examples for AI Coding Agents

A coding agent needs more than a feature request. It needs to know which commands are safe, where architectural decisions live and what counts as finished. Without that context, an assistant can produce a.

Includes: JSON to TSSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Reviewing AI-Written Code: A Workflow for AI Assistants

Review AI-generated changes with a repeatable workflow for defining scope, verifying dependencies, testing behavior, and keeping human ownership of every merged line.

Includes: JSON to TSGrace ChenGrace Chen
Dev Tools & AI Coding

Essential VS Code Extensions for Frontend Developers

Build a focused VS Code setup with ESLint, Prettier, framework support, and repeatable workspace settings, without piling on conflicting extensions.

Includes: JSON to TSRavi DesaiRavi Desai