Введение: почему рефакторинг legacy кода на TypeScript — это необходимость
Работа с унаследованным (legacy) кодом — одна из самых частых и сложных задач разработчика. Часто такой код написан на чистом JavaScript без строгой типизации, содержит запутанные зависимости и отсутствие модульной структуры. Переход на TypeScript не просто добавляет типы — это фундаментальное улучшение качества кода, которое снижает количество багов, упрощает рефакторинг и ускоряет онбординг новых разработчиков.
В этой статье мы разберем пошаговый процесс рефакторинга legacy кода на TypeScript. Вы узнаете, как безопасно внедрять типы, разбивать монолитные функции и тестировать изменения.
Шаг 1. Анализ и подготовка legacy кода
Прежде чем писать код, необходимо понять текущую архитектуру. Без этого рефакторинг превратится в хаос.
Создайте карту зависимостей
Начните с анализа файлов и модулей. Определите:
- Какие функции являются "чистыми" (pure), а какие имеют побочные эффекты.
- Где находятся глобальные переменные и состояния.
- Какие участки кода дублируются.
Напишите тесты для критических участков
Рефакторинг без тестов — это игра в русскую рулетку. Напишите хотя бы модульные тесты для ключевых функций. Пример теста на TypeScript:
// Исходный legacy код (JavaScript)
function calculatePrice(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
return total;
}
// Тест на TypeScript (Jest)
describe('calculatePrice', () => {
it('should calculate total price correctly', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 }
];
expect(calculatePrice(items)).toBe(35);
});
});
Если тесты проходят — можно начинать рефакторинг.
Шаг 2. Постепенное внедрение строгой типизации
Не пытайтесь покрыть типами всё сразу. Используйте стратегию "снизу вверх": начните с самых простых и изолированных функций.
Типизация простых функций
Добавляйте типы для параметров и возвращаемых значений. Пример:
// Было (JavaScript)
function greet(name) {
return "Hello, " + name;
}
// Стало (TypeScript)
function greet(name: string): string {
return `Hello, ${name}`;
}
Работа с объектами и интерфейсами
Для сложных структур данных создавайте интерфейсы. Это делает код самодокументируемым:
// Определяем интерфейс для элемента заказа
interface OrderItem {
id: number;
name: string;
price: number;
quantity: number;
}
// Функция с типизированным параметром
function getTotalPrice(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
Используйте `any` только как временное решение
Тип any — враг строгой типизации. Если вы не знаете точный тип, используйте unknown и проверяйте его в runtime:
function safeParse(data: unknown): string {
if (typeof data === 'string') {
return data.trim();
}
return '';
}
Шаг 3. Разделение монолитных функций и модулей
Legacy код часто содержит "божественные функции" (god functions), которые делают всё сразу. Разбивайте их на маленькие, переиспользуемые модули.
Выделение чистой логики
Пример: функция, которая валидирует email и отправляет запрос. Разделим её:
// Было (монолит)
function processUser(email) {
if (!email || !email.includes('@')) {
throw new Error('Invalid email');
}
// ... отправка запроса
console.log('Sending request...');
}
// Стало (разделение ответственности)
function validateEmail(email: string): boolean {
return email.includes('@') && email.length > 5;
}
async function sendUserRequest(email: string): Promise<void> {
if (!validateEmail(email)) {
throw new Error('Invalid email');
}
// ... отправка запроса
console.log('Sending request...');
}
Создание утилитарных модулей
Выносите повторяющийся код в отдельные файлы. Например, функции форматирования дат или работы с API:
// utils/date.ts
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
// utils/api.ts
export async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
Шаг 4. Рефакторинг с использованием современных возможностей TypeScript
После базовой типизации можно внедрять продвинутые паттерны. Это повысит читаемость и безопасность кода.
Discriminated Unions (размеченные объединения)
Используйте для обработки разных состояний (например, успех/ошибка):
// Вместо проверки на null
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function getUser(id: number): Result<User> {
try {
const user = database.findUser(id);
return { success: true, data: user };
} catch (err) {
return { success: false, error: err.message };
}
}
// Использование
const result = getUser(1);
if (result.success) {
console.log(result.data.name); // TypeScript знает, что data существует
} else {
console.error(result.error);
}
Generic типы для переиспользования
Обобщённые типы позволяют создавать универсальные функции без потери типовой безопасности:
// Обёртка для ответа API
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
// Функция, которая возвращает типизированный ответ
function parseResponse<T>(json: string): ApiResponse<T> {
return JSON.parse(json) as ApiResponse<T>;
}
// Использование
const response = parseResponse<User[]>('{"status": 200, "data": [...]}');
// response.data — массив User