En esta página
Migración de JavaScript a TypeScript
¿Por qué migrar de JavaScript a TypeScript?
Migrar un proyecto JavaScript existente a TypeScript puede parecer una tarea desalentadora, pero los beneficios son concretos: errores detectados en tiempo de compilación en lugar de producción, autocompletado preciso, refactoring seguro y documentación viva a través de los tipos. La clave está en hacerlo de forma gradual, sin detener el desarrollo ni generar cientos de errores de golpe.
Estrategias de migración
Existen dos enfoques principales:
| Estrategia | Descripción | Ideal para |
|---|---|---|
| Gradual | Habilitas TypeScript junto a JS existente, migras archivo a archivo | Proyectos grandes en producción activa |
| Completa | Conviertes todo de una vez | Proyectos pequeños o con tiempo de pausa disponible |
La estrategia gradual es la más común y es la que aprenderás en esta lección.
Paso 1: Instalar TypeScript y crear tsconfig.json
npm install --save-dev typescript @types/node
npx tsc --initConfiguración inicial permisiva para proyectos JavaScript existentes:
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"allowJs": true,
"checkJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Con allowJs: true, TypeScript compila tanto archivos .js como .ts. Con checkJs: false, los archivos JS no se verifican tipado inicialmente.
Paso 2: Activar checkJs de forma gradual
En lugar de activar checkJs globalmente (lo que generaría muchos errores), puedes activarlo archivo por archivo añadiendo la directiva al inicio del fichero JS:
// src/utils/formatear.js
// @ts-check ← activa verificación en este archivo específico
/**
* Formatea un precio con moneda.
* @param {number} precio
* @param {string} moneda
* @returns {string}
*/
function formatearPrecio(precio, moneda) {
return new Intl.NumberFormat("es-ES", {
style: "currency",
currency: moneda,
}).format(precio);
}
module.exports = { formatearPrecio };Los comentarios JSDoc @param y @returns son reconocidos por TypeScript para inferir tipos en archivos JS con @ts-check.
Paso 3: Renombrar archivos .js → .ts
Empieza por los módulos más pequeños y sin dependencias, generalmente las utilidades y los modelos:
# Orden recomendado:
# 1. Tipos / modelos / interfaces
# 2. Utilidades puras (sin efectos secundarios)
# 3. Servicios y repositorios
# 4. Controladores / rutas
# 5. Punto de entrada (app.js → app.ts)Al renombrar un archivo, TypeScript mostrará errores donde los tipos son any implícito o donde hay incompatibilidades. Corrígelos progresivamente.
Paso 4: Aumentar la severidad del tsconfig
A medida que migras archivos, endurece el tsconfig.json progresivamente:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": false,
"noUnusedParameters": false
}
}Luego, cuando la mayoría del código esté migrado:
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}@ts-ignore vs @ts-expect-error
Cuando encuentres un error que no puedes resolver de inmediato (deuda técnica), tienes dos directivas:
// @ts-ignore: suprime CUALQUIER error en la siguiente línea
// ⚠️ No avisa si el error desaparece (puede quedar obsoleta)
// @ts-ignore
const resultado = funcionLegada(dato);
// @ts-expect-error: documenta que ESPERAS un error
// ✅ TypeScript avisa si el error se resuelve y la directiva ya no es necesaria
// @ts-expect-error TODO: tipar funcionLegada en [issue #42]
const resultado2 = funcionLegada(dato);Usa siempre @ts-expect-error con un comentario que explique el contexto y, si es posible, un enlace al ticket donde está registrada la deuda.
Tipar librerías de terceros sin tipos
DefinitelyTyped (@types/*)
La mayoría de librerías populares tienen paquetes de tipos en DefinitelyTyped:
# Express
npm install --save-dev @types/express
# Node.js
npm install --save-dev @types/node
# Lodash
npm install --save-dev @types/lodash
# Jest
npm install --save-dev @types/jestComprobar si una librería tiene tipos propios
# Opción 1: buscar en node_modules
ls node_modules/mi-libreria/index.d.ts
# Opción 2: consultar el campo "types" en package.json
cat node_modules/mi-libreria/package.json | grep '"types"'
# Opción 3: buscar en DefinitelyTyped
npx @definitelytyped/dts-critic mi-libreriaCrear tipos para una librería sin soporte
// types/libreria-sin-tipos.d.ts
declare module "libreria-sin-tipos" {
export interface OpcionesCliente {
baseUrl: string;
apiKey: string;
timeout?: number;
}
export interface RespuestaBase<T = unknown> {
datos: T;
meta: {
total: number;
pagina: number;
};
}
export class Cliente {
constructor(opciones: OpcionesCliente);
get<T>(ruta: string): Promise<RespuestaBase<T>>;
post<T>(ruta: string, cuerpo: unknown): Promise<RespuestaBase<T>>;
put<T>(ruta: string, cuerpo: unknown): Promise<RespuestaBase<T>>;
delete(ruta: string): Promise<void>;
}
export const version: string;
export default Cliente;
}Ejemplo completo: migración de una app Express
A continuación verás el proceso completo de migrar una pequeña API Express desde JavaScript puro hasta TypeScript estricto.
Estado inicial (JavaScript)
// app.js — antes de la migración
const express = require("express");
const app = express();
app.use(express.json());
const usuarios = [];
let siguienteId = 1;
app.get("/usuarios", (req, res) => {
res.json(usuarios);
});
app.post("/usuarios", (req, res) => {
const { nombre, email } = req.body;
if (!nombre || !email) {
return res.status(400).json({ error: "Nombre y email son obligatorios" });
}
const usuario = { id: siguienteId++, nombre, email };
usuarios.push(usuario);
res.status(201).json(usuario);
});
app.get("/usuarios/:id", (req, res) => {
const usuario = usuarios.find(u => u.id === parseInt(req.params.id));
if (!usuario) return res.status(404).json({ error: "No encontrado" });
res.json(usuario);
});
app.listen(3000);Estado final (TypeScript estricto)
// app.ts — después de la migración completa
import express from "express";
import type { Request, Response, NextFunction } from "express";
// ──────────────────────────────────────────────────────────────
// Tipos del dominio
// ──────────────────────────────────────────────────────────────
interface Usuario {
id: number;
nombre: string;
email: string;
creadoEn: Date;
}
interface CrearUsuarioBody {
nombre: string;
email: string;
}
interface ErrorResponse {
error: string;
codigo?: string;
}
// ──────────────────────────────────────────────────────────────
// Repositorio en memoria
// ──────────────────────────────────────────────────────────────
class RepositorioUsuarios {
private usuarios: Usuario[] = [];
private siguienteId = 1;
obtenerTodos(): Usuario[] {
return [...this.usuarios];
}
obtenerPorId(id: number): Usuario | undefined {
return this.usuarios.find((u) => u.id === id);
}
crear(datos: CrearUsuarioBody): Usuario {
const usuario: Usuario = {
...datos,
id: this.siguienteId++,
creadoEn: new Date(),
};
this.usuarios.push(usuario);
return usuario;
}
}
// ──────────────────────────────────────────────────────────────
// Aplicación
// ──────────────────────────────────────────────────────────────
const app = express();
const repo = new RepositorioUsuarios();
app.use(express.json());
// GET /usuarios
app.get("/usuarios", (_req: Request, res: Response<Usuario[]>) => {
res.json(repo.obtenerTodos());
});
// POST /usuarios
app.post(
"/usuarios",
(req: Request<object, Usuario | ErrorResponse, CrearUsuarioBody>, res: Response) => {
const { nombre, email } = req.body;
if (!nombre?.trim() || !email?.trim()) {
res.status(400).json({ error: "Nombre y email son obligatorios" });
return;
}
if (!email.includes("@")) {
res.status(400).json({ error: "El email no tiene un formato válido" });
return;
}
const usuario = repo.crear({ nombre: nombre.trim(), email: email.trim() });
res.status(201).json(usuario);
}
);
// GET /usuarios/:id
app.get(
"/usuarios/:id",
(req: Request<{ id: string }>, res: Response<Usuario | ErrorResponse>) => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
res.status(400).json({ error: "El id debe ser un número" });
return;
}
const usuario = repo.obtenerPorId(id);
if (!usuario) {
res.status(404).json({ error: `Usuario con id ${id} no encontrado` });
return;
}
res.json(usuario);
}
);
// Manejador de errores global
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error("Error no manejado:", err);
res.status(500).json({ error: "Error interno del servidor" });
});
// Arranque
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Servidor escuchando en http://localhost:${PORT}`);
});
export default app;Lista de verificación de migración
Sigue este orden para una migración sin sorpresas:
- Instala dependencias:
typescript,@types/node, tipos de tus librerías principales. - Crea
tsconfig.jsonconallowJs: true,strict: false. - No rompe nada: compila el proyecto y asegúrate de que el JavaScript existente sigue funcionando.
- Activa
@ts-checken archivos pequeños y añade JSDoc donde sea útil. - Renombra archivos de más atómicos a más complejos: modelos → utilidades → servicios → rutas → app.
- Corrige errores archivos por archivos; usa
@ts-expect-errorpara los que no puedes resolver aún. - Endurece
tsconfig.json: activanoImplicitAny, luegostrictNullChecks, finalmentestrict: true. - Elimina directivas temporales: revisa todos los
@ts-expect-errory ciérralos cuando la deuda esté saldada. - Configura CI: añade
tsc --noEmitcomo paso del pipeline para evitar regresiones.
Con este proceso puedes migrar incluso proyectos grandes de forma segura, manteniendo la aplicación funcional en todo momento. En la lección final pondrás en práctica todos los conceptos del curso en un proyecto completo de gestión de tareas.
Inicia sesión para guardar tu progreso