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

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

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

Начать курс

Введение: Почему 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 проекта может сломать тысячи строк кода. Вместо этого включайте флаги по одному:

    <

Блоги

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