Si llevas tiempo escribiendo JavaScript, esta escena te sonará: cambias el nombre de una propiedad en un objeto, guardas, recargas el navegador y... todo parece ir bien. Hasta que dos días después, en producción, salta un Cannot read properties of undefined. El error no estaba en el código nuevo, sino en un archivo que ni recordabas que dependía de ese objeto.
JavaScript es un lenguaje dinámico: los tipos existen, pero solo se comprueban en tiempo de ejecución. Eso significa que muchos errores no aparecen hasta que el código ya está corriendo, muchas veces delante de un usuario real.
TypeScript es un superconjunto de JavaScript creado por Microsoft que añade un sistema de tipos estático opcional. Todo tu JavaScript ya es TypeScript válido, pero encima puedes describir la forma de tus datos y dejar que un compilador verifique que todo encaja antes de ejecutar nada. El código TypeScript se compila (o "transpila") a JavaScript plano que corre en cualquier sitio: navegador, Node.js, Deno o Bun.
A día de hoy la versión estable es TypeScript 6.0 (julio de 2026), y se ha convertido en el estándar de facto para proyectos serios de frontend y backend en el ecosistema JavaScript.
Veamos el mismo código con y sin tipos. En JavaScript nada te impide llamar mal a una función:
function calcularTotal(precio, cantidad) {
return precio * cantidad;
}
// Nadie te avisa de nada:
calcularTotal("10", 5); // "1010101010" 😱 concatenación inesperada
calcularTotal(10); // NaN, falta un argumento
En TypeScript, describes qué esperas recibir y el error salta mientras escribes, no en producción:
function calcularTotal(precio: number, cantidad: number): number {
return precio * cantidad;
}
calcularTotal("10", 5);
// ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'.
calcularTotal(10);
// ❌ Error: Expected 2 arguments, but got 1.
Ninguno de estos programas llega a ejecutarse con el bug dentro. El compilador los detiene antes.
La verdadera potencia aparece con los objetos. Puedes describir la estructura de un dato una sola vez y reutilizarla:
interface Usuario {
id: number;
nombre: string;
email: string;
activo: boolean;
}
function saludar(usuario: Usuario): string {
return `Hola, ${usuario.nombre}`;
}
saludar({ id: 1, nombre: "Alex", email: "a@b.com", activo: true }); // ✅
saludar({ id: 1, nombre: "Alex" });
// ❌ Error: faltan las propiedades 'email' y 'activo'.
Además de interface tienes type, que es más flexible para uniones y composiciones. Una regla práctica: usa interface para la forma de objetos y type cuando necesites uniones o alias:
type Estado = "cargando" | "exito" | "error";
function render(estado: Estado) {
// Autocompletado te ofrece solo esos tres valores
}
render("exito"); // ✅
render("completado"); // ❌ no es un valor válido de Estado
Ese tipo Estado es un union type: una restricción imposible de expresar con comentarios en JavaScript, y que elimina toda una categoría de errores por strings mágicos.
Un malentendido común es pensar que TypeScript te obliga a escribir tipos por todas partes. No es así: infiere la mayoría por ti.
const numeros = [1, 2, 3]; // TS ya sabe que es number[]
const doble = numeros.map((n) => n * 2); // n es number, doble es number[]
const usuario = { nombre: "Alex", edad: 30 }; // { nombre: string; edad: number }
usuario.edad.toUpperCase();
// ❌ Error: 'toUpperCase' no existe en 'number'. Detectado sin anotar nada.
En la práctica anotas sobre todo los límites: parámetros de funciones, valores de retorno públicos y respuestas de APIs. El interior de tu lógica suele tiparse solo.
| Beneficio | Qué significa en el día a día |
|---|---|
| Autocompletado real | El editor conoce la forma exacta de cada objeto y te sugiere propiedades y métodos válidos. |
| Refactor seguro | Renombrar una propiedad o mover una función actualiza (o marca en rojo) todos los usos del proyecto. |
| Menos bugs en runtime | Toda una clase de errores (undefined, argumentos mal pasados, typos) se detecta al escribir. |
| Documentación viva | Los tipos describen el contrato del código sin comentarios que se quedan obsoletos. |
| Mantenibilidad en equipo | Al abrir una función sabes qué recibe y qué devuelve sin leer su implementación. |
El punto del autocompletado merece énfasis: no es un extra cosmético. Cuando el editor sabe que usuario es un Usuario, escribir usuario. te muestra al instante id, nombre, email y activo. Menos idas y venidas a la documentación, menos errores tipográficos, más velocidad.
La refactorización segura es quizá el beneficio que más se nota al crecer un proyecto. En JavaScript, renombrar un campo implica buscar por texto y rezar. En TypeScript, si algo se rompe, lo ves en rojo antes de guardar.
"Añade complejidad y ceremonia." Al principio escribes algo más, sí. Pero ese coste se concentra en las fronteras del código (funciones, modelos de datos) y se paga solo cuando el proyecto crece o entra más gente. La inferencia se encarga de la mayor parte del resto.
"Con tests ya cubro los errores." Los tests y los tipos son complementarios, no rivales. Los tipos eliminan gratis toda una categoría de fallos (formas incorrectas, undefined, typos) que tendrías que cubrir con tests aburridos y repetitivos. Así tus tests se centran en la lógica de negocio, que es donde de verdad aportan.
"Me frena, quiero ir rápido." Vas más lento el primer día y más rápido el resto del proyecto: menos depuración en el navegador, menos regresiones y refactors sin miedo. En equipo, la ganancia es aún mayor.
"La curva de aprendizaje." Es real, pero más suave de lo que parece. Si ya sabes JavaScript, en una tarde eres productivo: anotar parámetros, crear una interface y leer los errores del compilador cubre el 80% del uso diario. Conceptos avanzados como generics o conditional types pueden esperar; no los necesitas para empezar a ganar.
La mejor noticia: no tienes que migrar tu proyecto de golpe. TypeScript está diseñado para una adopción gradual.
tsconfig.jsonnpm install -D typescript
npx tsc --init
allowJsUn tsconfig.json cómodo para convivir con JavaScript existente:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Las dos opciones clave para migrar:
allowJs: permite que archivos .js y .ts convivan en el mismo proyecto. Puedes convertir un archivo cada vez, sin romper nada.checkJs: cuando estés listo, ponlo en true para que TypeScript también revise tus .js (apoyándose en JSDoc). Es un buen paso intermedio antes de renombrar a .ts.Sobre strict: actívalo desde el principio en proyectos nuevos. Agrupa comprobaciones como noImplicitAny y strictNullChecks, que son justamente las que cazan los undefined que más duelen. En una migración, puedes empezar con strict: false e ir apretando tuerca por tuerca.
.ts vs .tsxLa regla es simple:
.ts para código normal (lógica, utilidades, servidor)..tsx para archivos que contengan JSX, es decir, componentes de React.interface Props {
nombre: string;
}
export function Saludo({ nombre }: Props) {
return <h1>Hola, {nombre}</h1>;
}
Si usas Next.js, Vite o cualquier framework moderno, el soporte de TypeScript viene integrado: normalmente basta con crear el tsconfig.json (o incluso se genera solo) y empezar a renombrar archivos.
Renombra un .js a .ts y observa lo que el compilador marca en rojo. Cada error es un bug potencial que JavaScript te habría dejado pasar. Arréglalos uno a uno; no hace falta hacerlo todo en un día.
TypeScript no es un lenguaje nuevo que tengas que aprender desde cero: es tu JavaScript de siempre con una red de seguridad encima. La inversión inicial es pequeña —anotar parámetros, escribir alguna interface, leer los errores del compilador— y el retorno es enorme: autocompletado real, refactors sin miedo, menos bugs en producción y un código que se explica solo cuando otra persona (o tu yo del futuro) lo abre.
Lo mejor es que no hay que apostarlo todo de una vez. Instala TypeScript, activa allowJs y convierte un solo archivo esta semana. Cuando veas el primer bug que el compilador caza antes de llegar al navegador, entenderás por qué tanta gente ya no vuelve atrás.
React 19 introduce use(), un nuevo primitivo que permite leer promesas y contextos directamente durante el render. Adiós a useEffect para cargar datos.
Next.js 16 llega con Turbopack estable, params/searchParams totalmente async y el cambio de middleware a proxy. Repaso los pasos y el codemod para migrar sin sustos.