Saltar al contenido

Migración incremental de JavaScript a TypeScript

Guía paso a paso para migrar proyectos JavaScript a TypeScript sin detener el desarrollo: configuración, adopción del modo estricto y flujos de equipo.

4 min de lectura
Línea de tiempo de migración que muestra archivos JavaScript convirtiéndose progresivamente a TypeScript a través de etapas de modo estricto

Migrar una base de código JavaScript a TypeScript no requiere una reescritura total de golpe. Las migraciones más exitosas ocurren de forma incremental, un archivo a la vez, y cada paso añade seguridad de tipos mientras la aplicación se mantiene funcional. Puedes seguir lanzando funcionalidades durante la migración, no solo después de ella.

La idea clave: TypeScript es un superconjunto de JavaScript. Cualquier archivo .js es TypeScript válido con cambios mínimos. Empieza en modo permisivo y ve endureciendo la exigencia a medida que aumenta la cobertura.

Fase 1: Configuración sin cambiar código

Antes de tocar un solo archivo JavaScript, configura TypeScript para que acepte tu base de código existente tal cual.

jsonjson
// tsconfig.json — permissive starting point
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": true,          // Allow .js files alongside .ts
    "checkJs": false,         // Don't type-check .js files yet
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": false,          // Start permissive
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
shbash
# Install TypeScript and type definitions for your dependencies
npm install -D typescript @types/node @types/react @types/express
# Check which @types packages exist for your dependencies
npx typesync

En este punto, nada cambia para el código existente. Los archivos JavaScript se compilan a través de TypeScript sin modificaciones. El build funciona. Las pruebas pasan. Despliégalo.

Fase 2: Renombra archivos de forma estratégica

Empieza a renombrar archivos .js a .ts (o .jsx a .tsx) comenzando por los módulos hoja — archivos que importan a otros pero a los que casi nadie importa.

## Migration order (leaf → root):

1. Utility functions (src/utils/*.js → *.ts)
   - Pure functions, few dependencies, easy to type

2. Data types and constants (src/types/*.js → *.ts)
   - Define interfaces that other files will use

3. Service layers (src/services/*.js → *.ts)
   - Database queries, API clients

4. Business logic (src/domain/*.js → *.ts)
   - Core application logic

5. Route handlers / components (src/routes/*.js → *.ts)
   - Entry points that compose everything

6. Entry point (src/index.js → src/index.ts)
   - Last file to convert
tstypescript
// ❌ Converting a file and adding full types immediately
// This blocks the migration because you need types for everything
 
// ✅ Rename and add minimal types — fix the rest incrementally
// Before: src/utils/format.js
export function formatCurrency(amount) {
  return `$${amount.toFixed(2)}`;
}
 
// After: src/utils/format.ts
export function formatCurrency(amount: number): string {
  return `$${amount.toFixed(2)}`;
}
// Quick win — function signature typed, behavior unchanged

Fase 3: Define las interfaces principales

Crea definiciones de tipos para tus objetos de dominio cuanto antes. Estas interfaces las usará cada archivo que conviertas a partir de ahora.

tstypescript
// src/types/domain.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user' | 'moderator';
  createdAt: Date;
  updatedAt: Date;
}
 
export interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}
 
export interface Order {
  id: string;
  userId: string;
  items: OrderItem[];
  total: number;
  status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
  createdAt: Date;
}
 
export interface OrderItem {
  productId: string;
  quantity: number;
  unitPrice: number;
}
 
// API response wrapper
export interface ApiResponse<T> {
  data: T;
  meta?: {
    page: number;
    pageSize: number;
    total: number;
  };
}
 
export interface ApiError {
  error: string;
  status: number;
  details?: Record<string, string[]>;
}

Fase 4: Gestiona las librerías de terceros

Algunas librerías tienen paquetes @types. Otras incluyen tipos propios. Otras no tienen ninguno de los dos. Cada caso se trata de forma distinta.

tstypescript
// Case 1: Library has @types package — just install it
// npm install -D @types/lodash
import { groupBy } from 'lodash';
// Types work automatically
 
// Case 2: Library has built-in types — nothing to install
import axios from 'axios';
// axios ships its own .d.ts files
 
// Case 3: No types available — create a declaration file
// src/types/untyped-lib.d.ts
declare module 'legacy-csv-parser' {
  interface ParseOptions {
    delimiter?: string;
    headers?: boolean;
  }
 
  interface ParseResult {
    data: Record<string, string>[];
    errors: string[];
  }
 
  export function parse(input: string, options?: ParseOptions): ParseResult;
}
 
// Now you can import with types:
import { parse } from 'legacy-csv-parser';
const result = parse(csvData, { delimiter: ',' });
// result.data is Record<string, string>[]
tstypescript
// ❌ Using 'any' to silence type errors on untyped libraries
const result: any = legacyLib.doSomething();
// Loses all type safety downstream
 
// ✅ Create a minimal declaration and improve it over time
// Start with what you actually use, not the full API surface
declare module 'legacy-lib' {
  export function doSomething(): { id: string; value: number };
}

Fase 5: Modo estricto incremental

El indicador strict de TypeScript es, en realidad, un conjunto de comprobaciones individuales. Actívalas una por una en lugar de todas de golpe.

jsonjson
// ❌ Enabling strict all at once — hundreds of errors
{
  "compilerOptions": {
    "strict": true  // Enables ALL strict checks simultaneously
  }
}
jsonjson
// ✅ Enable individual strict checks progressively
{
  "compilerOptions": {
    "strict": false,
 
    // Phase 1: Enable these first (easiest to fix)
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
 
    // Phase 2: Enable after Phase 1 is clean
    "noImplicitAny": true,           // Most impactful
    "strictNullChecks": true,         // Catches real bugs
 
    // Phase 3: Enable last (most strict)
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true
  }
}
tstypescript
// noImplicitAny — before:
function processItem(item) {    // 'item' implicitly has 'any' type
  return item.name.toUpperCase();
}
 
// noImplicitAny — after:
function processItem(item: Product): string {
  return item.name.toUpperCase();
}
 
// strictNullChecks — before:
function getUser(id: string): User {
  return users.find(u => u.id === id);  // Could be undefined!
}
 
// strictNullChecks — after:
function getUser(id: string): User | undefined {
  return users.find(u => u.id === id);
}
 
// Callers must handle the undefined case:
const user = getUser('123');
if (!user) {
  throw new Error('User not found');
}
console.log(user.name); // TypeScript knows user is not undefined here

strictNullChecks es, por sí sola, la opción más valiosa. Detecta errores de null/undefined en tiempo de compilación que de otro modo serían fallos en producción.

Seguimiento del progreso de la migración

Mide el progreso para que el equipo se mantenga motivado y la dirección entienda la inversión realizada.

shbash
#!/bin/bash
# scripts/migration-progress.sh
 
TOTAL_JS=$(find src -name "*.js" -o -name "*.jsx" | wc -l)
TOTAL_TS=$(find src -name "*.ts" -o -name "*.tsx" | wc -l)
TOTAL=$((TOTAL_JS + TOTAL_TS))
 
if [ $TOTAL -gt 0 ]; then
  PERCENT=$((TOTAL_TS * 100 / TOTAL))
else
  PERCENT=0
fi
 
echo "Migration Progress:"
echo "  JavaScript files: $TOTAL_JS"
echo "  TypeScript files: $TOTAL_TS"
echo "  Total: $TOTAL"
echo "  Progress: ${PERCENT}%"
echo ""
echo "Type coverage:"
npx type-coverage --detail

Ejecuta esto en CI y sigue el porcentaje a lo largo del tiempo. Una línea que crece de forma constante resulta motivadora. Una línea plana indica que la migración se ha estancado y necesita atención.

Conclusiones clave

  1. Empieza con allowJs: true y strict: false — deja que TypeScript acepte el JavaScript existente tal cual
  2. Convierte primero los módulos hoja — utilidades y tipos antes que los manejadores de rutas y los puntos de entrada
  3. Define pronto las interfaces de dominio — los tipos compartidos aceleran la conversión de cada archivo posterior
  4. Activa las comprobaciones estrictas de forma incremental — primero noImplicitAny y strictNullChecks, luego el resto
  5. Crea archivos de declaración para librerías sin tipos — tipa solo lo que uses, no toda la API
  6. Haz seguimiento del progreso en CI — mide el porcentaje de archivos TypeScript y la cobertura de tipos para mantener el impulso
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX