Введение: Почему legacy код на TypeScript требует особого подхода
Работа с унаследованным (legacy) кодом — неизбежная реальность для любого разработчика, особенно в крупных проектах. Часто такой код писался без строгой типизации, с использованием any в каждом углу, и представляет собой запутанный клубок зависимостей. Простой рефакторинг здесь не работает — нужен системный, продвинутый подход.
В этой статье мы рассмотрим стратегии рефакторинга legacy кода на TypeScript, которые выходят за рамки базовых руководств. Вы узнаете, как безопасно вводить строгую типизацию, изолировать изменения с помощью паттернов и использовать возможности компилятора для автоматизации рутинных задач.
Стратегия 1: Постепенное введение строгой типизации через "типовые мосты"
Одна из главных проблем legacy кода — тотальное использование any. Попытка заменить все any на конкретные типы за один раз приведет к сотням ошибок и параличу разработки. Вместо этого используем технику "типовых мостов".
Суть метода: мы не переписываем весь модуль сразу, а создаем промежуточные типы-мосты, которые постепенно вытесняют any.
Пример: Типизация "серого" модуля
Допустим, у нас есть старый модуль для работы с пользователями:
// legacyModule.ts
export function processUser(data: any): any {
// ... много нечитаемого кода
return { id: data.id, name: data.name, age: data.age };
}
Вместо того чтобы сразу типизировать всю функцию, создаем UserData и User:
// types.ts
export interface UserData {
id: number;
name: string;
age?: number; // поле может отсутствовать
}
export interface User {
id: number;
fullName: string;
isAdult: boolean;
}
Теперь используем эти типы, но оставляем старую сигнатуру для обратной совместимости:
// legacyModule.ts (рефакторинг шаг 1)
import { UserData, User } from './types';
export function processUser(data: UserData): User {
const isAdult = data.age ? data.age >= 18 : false;
return {
id: data.id,
fullName: data.name,
isAdult,
};
}
// Старая сигнатура для совместимости
export function processUserLegacy(data: any): any {
return processUser(data as UserData);
}
После того как все потребители перешли на новую функцию, удаляем processUserLegacy. Этот подход минимизирует риск поломки зависимостей.
Стратегия 2: Изоляция "грязных" зависимостей с помощью паттерна "Адаптер"
Legacy код часто тесно связан с внешними библиотеками, глобальными состояниями или устаревшими API. Чтобы безопасно проводить рефакторинг, нужно изолировать эти зависимости. Лучший инструмент для этого — паттерн "Адаптер" в сочетании с интерфейсами.
Пример: Изоляция устаревшего API
// oldApi.ts (legacy)
export function fetchOldData(callback: (data: any) => void): void {
// старый API с колбэками
setTimeout(() => callback({ value: 42 }), 100);
}
Создаем интерфейс для нового подхода (Promise-based) и адаптер:
// apiAdapter.ts
import { fetchOldData } from './oldApi';
export interface IDataFetcher {
fetchData(): Promise<{ value: number }>;
}
export class LegacyApiAdapter implements IDataFetcher {
fetchData(): Promise<{ value: number }> {
return new Promise((resolve, reject) => {
fetchOldData((data) => {
if (data) {
resolve(data);
} else {
reject(new Error('No data'));
}
});
});
}
}
Теперь весь новый код использует IDataFetcher. Когда вы полностью перепишете старый API, вам нужно будет только заменить реализацию адаптера, не трогая остальной код. Это снижает риск регрессии до минимума.
Стратегия 3: Автоматизация рефакторинга с помощью кодогенерации и утилит TypeScript
Продвинутый рефакторинг — это не только ручная работа. TypeScript предоставляет мощные инструменты для автоматизации преобразований. Используйте утилитарные типы и кодогенерацию для массовых замен.
3.1. Утилитарные типы для безопасного расширения
Часто в legacy коде объекты имеют необязательные поля, которые со временем стали обязательными. Вместо того чтобы править каждый файл, используем Required<T> и Partial<T>:
interface LegacyConfig {
host?: string;
port?: number;
timeout?: number;
}
// Новый код требует обязательные поля
type StrictConfig = Required<LegacyConfig>;
function setupServer(config: StrictConfig) {
// config.host, config.port, config.timeout — все обязательны
}
Это позволяет постепенно мигрировать, не ломая старые вызовы, которые используют Partial.
3.2. Кодогенерация для замены "any"
Если у вас есть большой JSON-файл с примерами данных, используйте генераторы типов (например, quicktype или json2ts) для создания интерфейсов. Затем напишите скрипт, который автоматически заменит any на сгенерированные типы в критических местах.
Пример скрипта (концептуально):
// replaceAny.ts (упрощенно)
import * as fs from 'fs';
import * as ts from 'typescript';
const source = fs.readFileSync('legacyFile.ts', 'utf-8');
// Используем компилятор TypeScript для AST-трансформации
// и замены всех 'any' на 'MyNewType'
// ... (реальная реализация требует работы с AST)
Этот подход требует навыков работы с компилятором TypeScript, но он незаменим для больших кодовых баз.
Стратегия 4: Использование строгих флагов компилятора и "strict mode" поэтапно
Включение strict: true в tsconfig.json для legacy проекта может сломать тысячи строк кода. Вместо этого включайте флаги по одному:
-
<