Рефакторинг legacy кода на TypeScript: пошаговое руководство

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

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

Начать курс

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

Работа с унаследованным (legacy) кодом — одна из самых частых и сложных задач разработчика. Часто такой код написан на чистом JavaScript без строгой типизации, содержит запутанные зависимости и отсутствие модульной структуры. Переход на TypeScript не просто добавляет типы — это фундаментальное улучшение качества кода, которое снижает количество багов, упрощает рефакторинг и ускоряет онбординг новых разработчиков.

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

Шаг 1. Анализ и подготовка legacy кода

Прежде чем писать код, необходимо понять текущую архитектуру. Без этого рефакторинг превратится в хаос.

Создайте карту зависимостей

Начните с анализа файлов и модулей. Определите:

  • Какие функции являются "чистыми" (pure), а какие имеют побочные эффекты.
  • Где находятся глобальные переменные и состояния.
  • Какие участки кода дублируются.

Напишите тесты для критических участков

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

// Исходный legacy код (JavaScript)
function calculatePrice(items) {
    let total = 0;
    for (let i = 0; i < items.length; i++) {
        total += items[i].price * items[i].quantity;
    }
    return total;
}

// Тест на 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);
    });
});

Если тесты проходят — можно начинать рефакторинг.

Шаг 2. Постепенное внедрение строгой типизации

Не пытайтесь покрыть типами всё сразу. Используйте стратегию "снизу вверх": начните с самых простых и изолированных функций.

Типизация простых функций

Добавляйте типы для параметров и возвращаемых значений. Пример:

// Было (JavaScript)
function greet(name) {
    return "Hello, " + name;
}

// Стало (TypeScript)
function greet(name: string): string {
    return `Hello, ${name}`;
}

Работа с объектами и интерфейсами

Для сложных структур данных создавайте интерфейсы. Это делает код самодокументируемым:

// Определяем интерфейс для элемента заказа
interface OrderItem {
    id: number;
    name: string;
    price: number;
    quantity: number;
}

// Функция с типизированным параметром
function getTotalPrice(items: OrderItem[]): number {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

Используйте `any` только как временное решение

Тип any — враг строгой типизации. Если вы не знаете точный тип, используйте unknown и проверяйте его в runtime:

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

Шаг 3. Разделение монолитных функций и модулей

Legacy код часто содержит "божественные функции" (god functions), которые делают всё сразу. Разбивайте их на маленькие, переиспользуемые модули.

Выделение чистой логики

Пример: функция, которая валидирует email и отправляет запрос. Разделим её:

// Было (монолит)
function processUser(email) {
    if (!email || !email.includes('@')) {
        throw new Error('Invalid email');
    }
    // ... отправка запроса
    console.log('Sending request...');
}

// Стало (разделение ответственности)
function validateEmail(email: string): boolean {
    return email.includes('@') && email.length > 5;
}

async function sendUserRequest(email: string): Promise<void> {
    if (!validateEmail(email)) {
        throw new Error('Invalid email');
    }
    // ... отправка запроса
    console.log('Sending request...');
}

Создание утилитарных модулей

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

// utils/date.ts
export function formatDate(date: Date): string {
    return date.toISOString().split('T')[0];
}

// utils/api.ts
export async function fetchData<T>(url: string): Promise<T> {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json() as Promise<T>;
}

Шаг 4. Рефакторинг с использованием современных возможностей TypeScript

После базовой типизации можно внедрять продвинутые паттерны. Это повысит читаемость и безопасность кода.

Discriminated Unions (размеченные объединения)

Используйте для обработки разных состояний (например, успех/ошибка):

// Вместо проверки на null
type Result<T> = 
    | { success: true; data: T }
    | { success: false; error: string };

function getUser(id: number): Result<User> {
    try {
        const user = database.findUser(id);
        return { success: true, data: user };
    } catch (err) {
        return { success: false, error: err.message };
    }
}

// Использование
const result = getUser(1);
if (result.success) {
    console.log(result.data.name); // TypeScript знает, что data существует
} else {
    console.error(result.error);
}

Generic типы для переиспользования

Обобщённые типы позволяют создавать универсальные функции без потери типовой безопасности:

// Обёртка для ответа API
interface ApiResponse<T> {
    status: number;
    data: T;
    message?: string;
}

// Функция, которая возвращает типизированный ответ
function parseResponse<T>(json: string): ApiResponse<T> {
    return JSON.parse(json) as ApiResponse<T>;
}

// Использование
const response = parseResponse<User[]>('{"status": 200, "data": [...]}');
// response.data — массив User

Заключение: как избежать ошибок при рефакторин

Блоги

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