Рефакторинг legacy кода на TypeScript: от А до Я
Работа с унаследованным (legacy) кодом — неизбежная реальность для многих разработчиков. Часто такие проекты написаны на "голом" JavaScript без типов, содержат запутанные зависимости и отсутствие тестов. TypeScript предлагает мощный инструментарий для постепенного улучшения такого кода, делая его безопаснее, читаемее и поддерживаемее. В этой статье мы пройдем путь от анализа старого кода до создания чистой, типизированной архитектуры.
1. Подготовка и стратегия: с чего начать?
Прежде чем менять код, необходимо создать "подушку безопасности". Главное правило рефакторинга: не ломай работающее. Начните с настройки окружения.
1.1. Инициализация TypeScript и строгий режим
Установите TypeScript и создайте файл конфигурации tsconfig.json. На первых порах можно отключить строгие проверки, чтобы не утонуть в ошибках, но постепенно их нужно включать.
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true, // Начните с false, затем постепенно включайте
"noImplicitAny": true,
"strictNullChecks": true,
"outDir": "./dist",
"rootDir": "./src",
"allowJs": true, // Разрешаем смешивать JS и TS
"checkJs": false // Пока не проверяем JS файлы
},
"include": ["src/**/*"]
}
1.2. Автоматизация и тесты
Добавьте линтер (ESLint) и форматтер (Prettier). Если тестов нет — напишите хотя бы интеграционные тесты для критичных участков. Это ваша страховка от регрессий.
- ESLint с плагином для TypeScript отловит логические ошибки.
- Jest или Vitest для модульного тестирования.
- Настройте pre-commit хуки (Husky), чтобы код проверялся перед коммитом.
2. Пошаговая миграция: от JS к TS
Не пытайтесь переписать всё сразу. Используйте итеративный подход. Лучший инструмент здесь — allowJs: true в tsconfig. Это позволяет подключать JavaScript файлы из TypeScript.
2.1. Переименование файлов и базовая типизация
Начните с переименования самых простых файлов utils.js или helpers.js в .ts. Компилятор начнет ругаться на неявные any. Ваша задача — описать сигнатуры функций.
До (JavaScript):
function calculateDiscount(price, percent) {
return price - (price * percent / 100);
}
После (TypeScript):
function calculateDiscount(price: number, percent: number): number {
return price - (price * percent / 100);
}
2.2. Работа с "any" и "unknown"
В legacy коде много неявных any. Не пытайтесь сразу всё типизировать идеально. Используйте any как временное решение, но помечайте это TODO. Для данных из внешних источников (API, localStorage) лучше использовать unknown с последующей проверкой типа.
// Временное решение
function processData(data: any) {
// TODO: Добавить правильный тип
console.log(data.name);
}
// Правильный подход для внешних данных
function processUserData(data: unknown) {
if (typeof data === 'object' && data !== null && 'id' in data) {
const user = data as { id: number; name: string };
console.log(user.name);
}
}
3. Рефакторинг сложных конструкций
Когда базовая типизация готова, переходите к улучшению архитектуры. Legacy код часто страдает от "божественных объектов" и запутанных цепочек вызовов.
3.1. Выделение интерфейсов и типов
Вместо передачи объектов с неизвестной структурой, создавайте интерфейсы. Это делает код самодокументируемым.
// Плохо
function saveUser(user: any) {
// Непонятно, какие поля есть у user
}
// Хорошо
interface IUser {
id: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
function saveUser(user: IUser): void {
// Работаем с четко определенной структурой
}
3.2. Замена "магических строк" на Enum и Union Types
Legacy код часто содержит строки, разбросанные по всему проекту. Замените их на перечисления.
// Вместо этого
if (status === 'active' || status === 'pending') { ... }
// Используйте это
type UserStatus = 'active' | 'pending' | 'blocked';
function isActive(status: UserStatus): boolean {
return status === 'active';
}
3.3. Улучшение работы с асинхронностью
Старый код на коллбэках или цепочках .then() стоит заменить на async/await с правильной обработкой ошибок.
// Было
function fetchData(callback: (err: Error | null, data?: any) => void) {
// ... логика
}
// Стало
async function fetchDataAsync<T>(): Promise<T> {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Network error');
return await response.json() as T;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
4. Продвинутые техники и заключение
4.1. Generics для переиспользуемого кода
Если вы видите, что функция или класс дублируются для разных типов данных, используйте дженерики. Это снижает количество кода и повышает его надежность.
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
// Теперь функция работает с любым типом
const num = getFirstElement([1, 2, 3]); // number
const str = getFirstElement(['a', 'b']); // string
4.2. Утилитарные типы (Partial, Pick, Omit)
Не создавайте десятки интерфейсов для разных состояний одного объекта. Используйте