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.

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.
// 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"]
}# 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 typesyncEn 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
// ❌ 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 unchangedFase 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.
// 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.
// 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>[]// ❌ 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.
// ❌ Enabling strict all at once — hundreds of errors
{
"compilerOptions": {
"strict": true // Enables ALL strict checks simultaneously
}
}// ✅ 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
}
}// 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 herestrictNullChecks 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.
#!/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 --detailEjecuta 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
- Empieza con
allowJs: trueystrict: false— deja que TypeScript acepte el JavaScript existente tal cual - Convierte primero los módulos hoja — utilidades y tipos antes que los manejadores de rutas y los puntos de entrada
- Define pronto las interfaces de dominio — los tipos compartidos aceleran la conversión de cada archivo posterior
- Activa las comprobaciones estrictas de forma incremental — primero
noImplicitAnyystrictNullChecks, luego el resto - Crea archivos de declaración para librerías sin tipos — tipa solo lo que uses, no toda la API
- Haz seguimiento del progreso en CI — mide el porcentaje de archivos TypeScript y la cobertura de tipos para mantener el impulso


