Refactoring Legacy Code in TypeScript: A Step-by-Step Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Refactoring Legacy Code to TypeScript is a Necessity

Working with legacy code is one of the most frequent and challenging tasks for a developer. Such code is often written in plain JavaScript without strict typing, contains tangled dependencies, and lacks a modular structure. Switching to TypeScript doesn't just add types — it's a fundamental improvement in code quality that reduces the number of bugs, simplifies refactoring, and accelerates onboarding for new developers.

In this article, we will break down the step-by-step process of refactoring legacy code to TypeScript. You will learn how to safely introduce types, break down monolithic functions, and test changes.



Step 1. Analysis and Preparation of Legacy Code

Before writing code, it is necessary to understand the current architecture. Without this, refactoring will turn into chaos.



Create a Dependency Map

Start by analyzing files and modules. Identify:

  • Which functions are "pure" and which have side effects.
  • Where global variables and states are located.
  • Which sections of code are duplicated.


Write Tests for Critical Sections

Refactoring without tests is playing Russian roulette. Write at least unit tests for key functions. Example of a test in TypeScript:

// Original legacy code (JavaScript)function calculatePrice(items) {    let total = 0;    for (let i = 0; i < items.length; i++) {        total += items[i].price * items[i].quantity;    }    return total;}

// Test in TypeScript (Jest)describe('calculatePrice', () => { it('should calculate total price correctly', () => { const items = [ { price: 10, quantity: 2 }, { price: 5, quantity: 3 } ]; expect(calculatePrice(items)).toBe(35); });});


If the tests pass, you can start refactoring.



Step 2. Gradual Introduction of Strict Typing

Don't try to cover everything with types at once. Use a "bottom-up" strategy: start with the simplest and most isolated functions.



Typing Simple Functions

Add types for parameters and return values. Example:

// Before (JavaScript)function greet(name) {    return "Hello, " + name;}

// After (TypeScript)function greet(name: string): string { return `Hello, ${name}`;}


Working with Objects and Interfaces

For complex data structures, create interfaces. This makes the code self-documenting:

// Define an interface for an order iteminterface OrderItem {    id: number;    name: string;    price: number;    quantity: number;}

// Function with a typed parameterfunction getTotalPrice(items: OrderItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0);}


Use `any` Only as a Temporary Solution

The any type is the enemy of strict typing. If you don't know the exact type, use unknown and check it at runtime:

function safeParse(data: unknown): string {    if (typeof data === 'string') {        return data.trim();    }    return '';}


Step 3. Splitting Monolithic Functions and Modules

Legacy code often contains "god functions" that do everything at once. Break them down into small, reusable modules.



Extracting Pure Logic

Example: a function that validates an email and sends a request. Let's split it:

// Before (monolithic)function processUser(email) {    if (!email || !email.includes('@')) {        throw new Error('Invalid email');    }    // ... sending request    console.log('Sending request...');}

// After (separation of concerns)function validateEmail(email: string): boolean { return email

Blogs

Book Recommendations