Рефакторинг legacy кода на TypeScript: от А до Я

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

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

Начать курс

Рефакторинг 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)

Не создавайте десятки интерфейсов для разных состояний одного объекта. Используйте

Блоги

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