Introducción: por qué la refactorización de código legacy a TypeScript es una necesidad
Trabajar con código heredado (legacy) es una de las tareas más frecuentes y complejas para un desarrollador. A menudo, este código está escrito en JavaScript puro sin tipado estricto, contiene dependencias enredadas y carece de una estructura modular. La migración a TypeScript no solo agrega tipos, sino que es una mejora fundamental en la calidad del código que reduce la cantidad de errores, simplifica la refactorización y acelera la incorporación de nuevos desarrolladores.
En este artículo, analizaremos el proceso paso a paso para refactorizar código legacy a TypeScript. Aprenderás cómo implementar tipos de forma segura, dividir funciones monolíticas y probar los cambios.
Paso 1. Análisis y preparación del código legacy
Antes de escribir código, es necesario comprender la arquitectura actual. Sin esto, la refactorización se convertirá en un caos.
Crea un mapa de dependencias
Comienza analizando los archivos y módulos. Determina:
- Qué funciones son "puras" (pure) y cuáles tienen efectos secundarios.
- Dónde se encuentran las variables y estados globales.
- Qué partes del código están duplicadas.
Escribe pruebas para las partes críticas
Refactorizar sin pruebas es jugar a la ruleta rusa. Escribe al menos pruebas unitarias para las funciones clave. Ejemplo de prueba en TypeScript:
// Código legacy original (JavaScript)function calculatePrice(items) { let total = 0; for (let i = 0; i < items.length; i++) { total += items[i].price * items[i].quantity; } return total;}
// Prueba en 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); });});Si las pruebas pasan, se puede comenzar la refactorización.
Paso 2. Implementación gradual del tipado estricto
No intentes cubrir todo con tipos de inmediato. Utiliza una estrategia "de abajo hacia arriba": comienza con las funciones más simples y aisladas.
Tipado de funciones simples
Agrega tipos para los parámetros y los valores de retorno. Ejemplo:
// Antes (JavaScript)function greet(name) { return "Hello, " + name;}
// Después (TypeScript)function greet(name: string): string { return `Hello, ${name}`;}Trabajo con objetos e interfaces
Para estructuras de datos complejas, crea interfaces. Esto hace que el código sea autodocumentado:
// Definimos la interfaz para un elemento del pedidointerface OrderItem { id: number; name: string; price: number; quantity: number;}
// Función con parámetro tipadofunction getTotalPrice(items: OrderItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0);}Usa `any` solo como solución temporal
El tipo any es enemigo del tipado estricto. Si no conoces el tipo exacto, usa unknown y verifícalo en tiempo de ejecución:
function safeParse(data: unknown): string { if (typeof data === 'string') { return data.trim(); } return '';}Paso 3. División de funciones y módulos monolíticos
El código legacy a menudo contiene "funciones divinas" (god functions) que lo hacen todo a la vez. Divídelas en módulos pequeños y reutilizables.
Aislamiento de la lógica pura
Ejemplo: una función que valida un email y envía una solicitud. Vamos a dividirla:
// Antes (monolítico)function processUser(email) { if (!email || !email.includes('@')) { throw new Error('Invalid email'); } // ... envío de solicitud console.log('Sending request...');}
// Después (separación de responsabilidades)function validateEmail(email: string): boolean { return email
```