Введение: почему рефакторинг legacy кода — это не роскошь, а необходимость
В современной веб-разработке TypeScript стал стандартом де-факто для крупных проектов. Однако многие компании и стартапы сталкиваются с проблемой: кодовая база, написанная на чистом JavaScript или ранних, нестрогих версиях TypeScript, со временем превращается в «болото». Отсутствие типов, раздутые функции, неявные зависимости и устаревшие паттерны — всё это замедляет разработку и увеличивает количество багов.
Рефакторинг legacy кода — это процесс улучшения внутренней структуры программы без изменения её внешнего поведения. В контексте TypeScript он приобретает особую ценность: мы не только чистим код, но и получаем мощную систему типов, которая предотвращает ошибки на этапе компиляции. В этой статье мы разберём пошаговый план рефакторинга, от первичного анализа до финального тестирования, с реальными примерами на TypeScript.
1. Анализ и инвентаризация: с чего начать?
Первый и самый важный шаг — понять, с чем мы имеем дело. Нельзя просто взять и переписать всё сразу. Это путь к хаосу и потерянному времени. Правильный подход — провести аудит кодовой базы.
- Определите «горячие точки». Найдите модули, которые чаще всего изменяются или содержат больше всего багов. Используйте инструменты вроде
git logили SonarQube. - Выявите неявные типы. В legacy JS-коде часто встречаются функции, которые принимают
anyили используют@ts-ignore. Составьте список таких мест. - Проверьте зависимости. Устаревшие библиотеки (например,
lodash3.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/,