Рефакторинг legacy кода на TypeScript: от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему рефакторинг legacy кода — это не роскошь, а необходимость

В современной веб-разработке TypeScript стал стандартом де-факто для крупных проектов. Однако многие компании и стартапы сталкиваются с проблемой: кодовая база, написанная на чистом JavaScript или ранних, нестрогих версиях TypeScript, со временем превращается в «болото». Отсутствие типов, раздутые функции, неявные зависимости и устаревшие паттерны — всё это замедляет разработку и увеличивает количество багов.

Рефакторинг legacy кода — это процесс улучшения внутренней структуры программы без изменения её внешнего поведения. В контексте TypeScript он приобретает особую ценность: мы не только чистим код, но и получаем мощную систему типов, которая предотвращает ошибки на этапе компиляции. В этой статье мы разберём пошаговый план рефакторинга, от первичного анализа до финального тестирования, с реальными примерами на TypeScript.

1. Анализ и инвентаризация: с чего начать?

Первый и самый важный шаг — понять, с чем мы имеем дело. Нельзя просто взять и переписать всё сразу. Это путь к хаосу и потерянному времени. Правильный подход — провести аудит кодовой базы.

  • Определите «горячие точки». Найдите модули, которые чаще всего изменяются или содержат больше всего багов. Используйте инструменты вроде git log или SonarQube.
  • Выявите неявные типы. В legacy JS-коде часто встречаются функции, которые принимают any или используют @ts-ignore. Составьте список таких мест.
  • Проверьте зависимости. Устаревшие библиотеки (например, lodash 3.x) могут мешать переходу на современные API TypeScript.
  • Напишите тесты. Перед любым рефакторингом покройте ключевые участки кода тестами. Это ваша «страховочная сетка».

Пример: у вас есть функция на JS, которая обрабатывает данные пользователя. Вместо того чтобы сразу её переписывать, добавьте юнит-тесты на Jest или Vitest:

// legacy.js
function processUser(data) {
    return { name: data.name, age: data.age + 1 };
}

// test.js
import { processUser } from './legacy';
test('processUser increments age', () => {
    expect(processUser({ name: 'Alice', age: 30 })).toEqual({ name: 'Alice', age: 31 });
});

Только после этого можно приступать к рефакторингу.

2. Постепенная миграция на строгую типизацию

TypeScript предлагает несколько уровней строгости. Для legacy кода рекомендуется начинать с "strict": false и постепенно повышать планку. Резкое включение всех опций может сломать сотни файлов.

Ключевые шаги:

  • Включите noImplicitAny. Это заставит вас явно указывать типы для всех параметров и возвращаемых значений. Сначала это может быть больно, но именно так вы найдёте самые опасные места.
  • Используйте strictNullChecks. Одна из главных причин багов — null и undefined. Этот флаг заставит вас обрабатывать их явно.
  • Добавьте noUnusedLocals и noUnusedParameters. Они помогут убрать мёртвый код.

Пример рефакторинга функции с неявными типами:

// До рефакторинга (legacy JS)
function calculateTotal(price, tax) {
    return price + (price * tax);
}

// После рефакторинга (TypeScript)
interface PriceData {
    price: number;
    tax: number;
}

function calculateTotal({ price, tax }: PriceData): number {
    if (price < 0 || tax < 0) {
        throw new Error('Price and tax must be non-negative');
    }
    return price + (price * tax);
}

Обратите внимание: мы не просто добавили типы, но и сделали API более безопасным, используя интерфейс и валидацию.

3. Избавление от антипаттернов и устаревших конструкций

Legacy код часто содержит конструкции, которые были популярны 5-10 лет назад, но сегодня считаются вредными. Вот список того, от чего стоит избавляться в первую очередь:

  • Мутирующие функции. Вместо изменения объектов на месте, возвращайте новые. Это улучшает предсказуемость и упрощает отладку.
  • Сложные цепочки .then(). Замените их на async/await — это делает код линейным и читаемым.
  • Глобальные переменные. Выносите состояние в модули или используйте DI (внедрение зависимостей).
  • Смешивание бизнес-логики и UI. Разделите код на слои: сервисы, репозитории, компоненты.

Пример рефакторинга асинхронного кода:

// До (Promise chain)
function fetchUserData(userId: string): Promise<User> {
    return fetch(`/api/users/${userId}`)
        .then(response => response.json())
        .then(data => {
            if (!data) throw new Error('User not found');
            return data;
        })
        .catch(err => {
            console.error(err);
            throw err;
        });
}

// После (async/await)
async function fetchUserData(userId: string): Promise<User> {
    try {
        const response = await fetch(`/api/users/${userId}`);
        const data: User = await response.json();
        if (!data) throw new Error('User not found');
        return data;
    } catch (error) {
        console.error('Failed to fetch user:', error);
        throw error;
    }
}

Второй вариант не только короче, но и легче для понимания. TypeScript при этом автоматически выводит тип User из ответа сервера (если он определён).

4. Оптимизация структуры проекта и модульности

Часто legacy проекты страдают от «плоской» структуры, где все файлы свалены в одну папку, или от «гигантских» файлов на 1000+ строк. Рефакторинг должен включать переосмысление архитектуры.

Рекомендации:

  • Группировка по фичам, а не по типам. Вместо папок components/,

Блоги

Рекомендаций по книгам