Рефакторинг legacy кода на TypeScript: пошаговое руководство
Работа с устаревшим (legacy) кодом — неизбежная часть жизни любого разработчика, особенно в крупных проектах. Часто такой код написан на JavaScript без строгой типизации, изобилует "магическими" строками, непонятными функциями и отсутствием модульности. Переход на TypeScript — это не просто смена синтаксиса, а возможность значительно улучшить архитектуру, читаемость и поддерживаемость проекта. В этой статье мы разберем проверенную стратегию рефакторинга legacy кода на TypeScript, которая минимизирует риски и приносит максимальную пользу.
Почему TypeScript идеален для рефакторинга legacy кода?
TypeScript предоставляет уникальные возможности для безопасного рефакторинга:
- Статическая типизация: Позволяет выявить ошибки на этапе компиляции, а не в рантайме.
- Автоматическое документирование: Типы служат живой документацией, объясняющей, какие данные ожидает функция.
- Улучшенная навигация и автодополнение: IDE (VS Code, WebStorm) понимает структуру кода, что ускоряет разработку.
- Безопасные рефакторинги: Переименование переменных или изменение сигнатур функций происходит с уверенностью, что ничего не сломается.
- Постепенное внедрение: TypeScript можно добавлять в проект файл за файлом, не переписывая всё сразу.
Шаг 1: Подготовка и настройка окружения
Прежде чем трогать код, необходимо подготовить инфраструктуру. Без неё рефакторинг превратится в хаос.
- Убедитесь, что у вас есть тесты. Это самое важное правило рефакторинга. Если тестов нет — напишите хотя бы интеграционные тесты для ключевых модулей. Без тестов вы рискуете сломать работающую функциональность.
- Настройте TypeScript. Установите
typescriptи создайтеtsconfig.json. - Включите строгие проверки. В
tsconfig.jsonобязательно добавьте"strict": true. Это включаетstrictNullChecks,noImplicitAnyи другие полезные опции.
Пример минимального tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
- Настройте линтер и форматтер. ESLint с плагином для TypeScript (
@typescript-eslint) и Prettier помогут поддерживать единый стиль кода.
Шаг 2: Постепенная миграция с JavaScript на TypeScript
Не пытайтесь переписать весь проект за один день. Используйте стратегию "файл за файлом".
Алгоритм действий:
- Переименуйте файл
legacyModule.jsвlegacyModule.ts. - Добавьте типы для экспортируемых и импортируемых функций.
- Используйте
anyвременно, если не уверены в типе. Пометьте такие места комментарием// TODO: fix type. - Запустите тесты и проверьте, что всё работает.
Пример: Предположим, у нас есть legacy функция на JavaScript:
// oldModule.js
function calculateDiscount(price, customerType) {
if (customerType === 'vip') {
return price * 0.8;
}
return price * 0.95;
}
После рефакторинга на TypeScript:
// oldModule.ts
type CustomerType = 'regular' | 'vip';
function calculateDiscount(price: number, customerType: CustomerType): number {
if (customerType === 'vip') {
return price * 0.8;
}
return price * 0.95;
}
Тип CustomerType делает код самодокументируемым. Теперь любой разработчик точно знает, какие значения может принимать параметр customerType.
Шаг 3: Улучшение архитектуры и устранение "запахов" кода
Когда базовые типы расставлены, можно переходить к более глубокому рефакторингу. Обратите внимание на следующие "запахи" кода (code smells):
- Дублирование кода: Выносите повторяющиеся участки в отдельные функции или классы.
- Длинные функции: Разбивайте их на несколько маленьких, каждая из которых отвечает за одну задачу.
- Магические числа и строки: Заменяйте их константами или перечислениями (
enum). - Сильная связанность (tight coupling): Используйте интерфейсы и внедрение зависимостей (Dependency Injection).
Пример рефакторинга с использованием интерфейса:
До рефакторинга (legacy код):
function sendEmail(userData) {
const transporter = nodemailer.createTransport({...});
// ... логика отправки
}
После рефакторинга (с интерфейсом и DI):
interface IMailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class EmailService implements IMailService {
async send(to: string, subject: string, body: string): Promise<void> {
// Реализация с nodemailer
}
}
class UserNotifier {
constructor(private mailService: IMailService) {}
async notifyUser(user: User): Promise<void> {
await this.mailService.send(user.email, 'Welcome!', '...');
}
}
Теперь UserNotifier не зависит от конкретной реализации EmailService. Вы можете легко заменить его на другой сервис (например, для тестов) или изменить логику отправки.
Шаг 4: Работа с "любыми" (any) и строгая типизация
После того как архитектура улучшена, приш