Lección 2 de 514 min
TypeScript: JavaScript con tipos
Inferencia, interfaces, uniones y narrowing, genéricos, tipos utilitarios y cómo se compila a JavaScript.
Cambias el nombre de una propiedad de precio a importe en un proyecto de trescientos archivos. En JavaScript te enteras de los sitios que olvidaste cuando un usuario ve «Total: NaN» en la pantalla. En TypeScript, el editor marca en rojo cada uso antiguo antes incluso de que guardes el archivo.
En el módulo anterior viste que TypeScript es JavaScript con tipos. Aquí aprenderás a escribir esos tipos: cuándo anotarlos y cuándo dejar que TypeScript los deduzca, cómo describir datos que pueden tener varias formas, cómo reutilizar tipos sin copiarlos y cómo se convierte todo en JavaScript. Igual que JavaScript, lo usarás tanto en el frontend (Angular, React) como en el backend (Node.js).
Describir la forma de los datos
Un tipo es una descripción de la forma que tiene un valor. TypeScript compara esa descripción con lo que haces realmente en el código y te avisa cuando no encajan.
Inferencia y anotaciones
TypeScript deduce la mayoría de tipos; tú anotas las fronteras: parámetros y datos externos.
Una anotación es el tipo que escribes tras dos puntos: let edad: number. Pero no hace falta ponerla en todas partes, porque TypeScript infiere el tipo a partir del valor. En const nombre = "Ana" ya sabe que es un texto, y en [1, 2, 3].map((n) => n * 2) sabe que n es un número.
La regla práctica: anota los parámetros de las funciones, que TypeScript no puede adivinar; los tipos de retorno de las funciones que otros van a usar; y los datos que llegan de fuera. Deja que infiera el resto. Para un valor del que no sabes nada, como el resultado de JSON.parse, usa unknown, que te obliga a comprobarlo antes de usarlo. any, en cambio, desactiva todas las comprobaciones.
Úsalo cuando
Anota cuando el tipo es una decisión de diseño, como la firma de una función o la forma de una respuesta de API. En las variables locales, la inferencia basta.
Ten en cuenta
any es contagioso: lo que sale de un any también es any y deja de comprobarse. Con strict activado, TypeScript no te deja olvidar el tipo de un parámetro, que de otro modo sería any sin avisar.
const precios = [10, 25, 40]; // inferido: number[]
const total = precios.reduce((s, p) => s + p, 0); // number
// Los parámetros sí se anotan
function conDescuento(precio: number, porcentaje: number): number {
return precio * (1 - porcentaje / 100);
}
// Datos externos: unknown obliga a comprobar antes de usar
const datos: unknown = JSON.parse(texto);
datos.nombre; // Error: 'datos' es de tipo 'unknown'.interface y type
Dos formas de dar nombre a un tipo; casi intercambiables cuando describen objetos.
interface Usuario { … } y type Usuario = { … } describen la misma forma de objeto y se usan igual. Una interfaz se amplía con extends; un alias de tipo, con una intersección (&). Los dos sirven para tipar props, respuestas de una API o el contrato que implementa una clase.
Las diferencias están en los bordes. Solo type puede nombrar cosas que no son objetos: uniones ("pendiente" | "pagado"), tuplas o tipos derivados de otros. Solo interface admite la fusión de declaraciones: si declaras dos veces la misma interfaz, TypeScript junta sus propiedades, algo útil para ampliar los tipos de una librería.
Úsalo cuando
Elige una convención y síguela. Muchos equipos usan interface para las formas de objeto y type para uniones y tipos derivados; otros usan type para todo. Lo importante es la coherencia.
Ten en cuenta
La fusión de declaraciones puede sorprenderte si reutilizas sin querer un nombre que ya existe, como Window o Event. Y ninguno de los dos existe al ejecutar: no puedes escribir x instanceof Usuario con ellos.
interface Producto {
id: number;
nombre: string;
precio: number;
}
interface ProductoDigital extends Producto {
urlDescarga: string;
}
// Solo con type: uniones, tuplas e intersecciones
type Estado = "pendiente" | "pagado" | "enviado";
type Coordenadas = [lat: number, lng: number];
type ProductoConEstado = Producto & { estado: Estado };Uniones y narrowing
Muchos valores reales pueden tener varias formas: un identificador que es un número o un texto, una respuesta que es un éxito o un error. TypeScript lo expresa con uniones y te ayuda a distinguir cada caso con comprobaciones normales de JavaScript.
Tipos unión
Un valor que puede ser de un tipo o de otro: string | number.
Una unión (A | B) dice que un valor es de un tipo o de otro. Es muy habitual con tipos literales: type Talla = "S" | "M" | "L" solo admite esos tres textos, y el editor te los autocompleta. También expresa valores que pueden faltar: string | null.
Sobre una unión solo puedes hacer lo que es válido para todos sus miembros. Si id es string | number, no puedes llamar a id.toUpperCase() sin más: primero tienes que comprobar que es un texto. Esa comprobación es el narrowing, que verás a continuación.
Úsalo cuando
Para valores con un conjunto cerrado de opciones (estados, tallas, roles) y para datos que pueden no estar.
Ten en cuenta
Para un conjunto cerrado de valores, una unión de literales suele ser mejor opción que un enum: no genera código al compilar y funciona con las herramientas que solo borran tipos, como verás al final de la lección.
type Talla = "S" | "M" | "L";
function etiqueta(id: string | number, talla: Talla) {
// id.toUpperCase(); // Error: no existe en el tipo 'number'
return `${id}-${talla}`;
}
etiqueta(42, "M"); // bien
etiqueta("a7", "XL"); // Error: '"XL"' no se puede asignar a 'Talla'.Narrowing con typeof e in
Comprobaciones de JavaScript que TypeScript entiende para afinar el tipo de un valor.
El narrowing (estrechamiento) es lo que hace TypeScript cuando lee un if: dentro de if (typeof id === "string") sabe que id es un texto y, en el else, que es el resto de la unión. No es una sintaxis nueva: son comprobaciones de JavaScript que el compilador sigue de cerca.
Las más usadas son typeof para primitivos, in para saber si un objeto tiene una propiedad ("ladrar" in mascota), instanceof para instancias de clases y la comparación con null. Si ninguna basta, puedes escribir tu propia comprobación con un predicado de tipo: una función cuyo tipo de retorno es valor is Tipo.
Úsalo cuando
Cada vez que trabajas con una unión o con unknown: antes de usar el valor, comprueba de qué tipo es.
Ten en cuenta
typeof null es "object", así que typeof x === "object" no descarta null; TypeScript te lo recordará. Y un predicado de tipo es una promesa que haces tú: si la función comprueba mal, TypeScript te creerá igualmente.
type Gato = { nombre: string; maullar(): void };
type Perro = { nombre: string; ladrar(): void };
function formatear(id: string | number) {
if (typeof id === "string") return id.toUpperCase(); // aquí, string
return id.toFixed(0); // aquí, number
}
function hablar(mascota: Gato | Perro) {
if ("ladrar" in mascota) mascota.ladrar(); // aquí, Perro
else mascota.maullar(); // aquí, Gato
}Uniones discriminadas
Una propiedad común (tipo, estado) que indica qué forma tiene cada caso.
Una unión discriminada es una unión de objetos que comparten una propiedad con un valor literal distinto en cada uno: el discriminante. Por ejemplo, { estado: "cargando" }, { estado: "ok", datos } y { estado: "error", mensaje }. Al comprobar estado con un if o un switch, TypeScript sabe exactamente qué otras propiedades existen en cada rama.
Es el patrón ideal para modelar estados, porque impide combinaciones imposibles, como tener a la vez datos y un mensaje de error. Y si en el default del switch asignas el valor a una variable de tipo never, TypeScript te avisará cuando alguien añada un caso nuevo y olvide tratarlo.
Úsalo cuando
Para los estados de una petición, mensajes o eventos de distintos tipos, las acciones de un store y resultados que pueden ser un éxito o un error.
Ten en cuenta
El discriminante tiene que ser un tipo literal ("ok", no string). Si construyes el objeto en una variable aparte, TypeScript puede ensanchar su tipo a string; una anotación o as const lo evitan.
type Respuesta =
| { estado: "cargando" }
| { estado: "ok"; datos: string[] }
| { estado: "error"; mensaje: string };
function pintar(r: Respuesta): string {
switch (r.estado) {
case "cargando":
return "Cargando…";
case "ok":
return `${r.datos.length} resultados`; // aquí existe «datos»
case "error":
return `Error: ${r.mensaje}`; // y aquí, «mensaje»
default: {
const imposible: never = r; // error si falta algún caso
return imposible;
}
}
}Reutilizar tipos
En un proyecto real los tipos se repiten con pequeñas variaciones: el producto completo, el producto sin id para crearlo, solo el nombre y el precio para un listado… En lugar de copiarlos, TypeScript te deja derivarlos unos de otros.
Genéricos
Tipos con parámetros: escribes una función o un tipo una vez y sirve para cualquier tipo.
Un genérico es un tipo que recibe otro tipo como parámetro, entre ángulos: Array<string>, Promise<Usuario>. Tú también puedes escribirlos: function primero<T>(lista: T[]): T | undefined funciona con listas de cualquier cosa y conserva el tipo: si le pasas string[], devuelve string | undefined.
Casi nunca hace falta indicar T al llamar a la función, porque TypeScript lo infiere de los argumentos. Puedes restringirlo con extends, como en <T extends { id: number }>, para exigir que tenga ciertas propiedades. Los genéricos están por todas partes: en los Map, en las promesas, en el tipo de las respuestas de una API.
Úsalo cuando
Cuando una función o un tipo trabajan igual con distintos tipos y quieres que el resultado mantenga el tipo concreto, en lugar de perderlo con any.
Ten en cuenta
Si T solo aparece una vez en la firma, probablemente no necesitas un genérico. Empieza con tipos concretos y generaliza cuando de verdad repitas código.
function primero<T>(lista: T[]): T | undefined {
return lista[0];
}
const n = primero([3, 1, 2]); // number | undefined
const s = primero(["a", "b"]); // string | undefined
// Con restricción: T debe tener un id numérico
function porId<T extends { id: number }>(items: T[], id: number) {
return items.find((item) => item.id === id); // T | undefined
}
type RespuestaApi<T> = { datos: T; pagina: number };
type ListaProductos = RespuestaApi<Producto[]>;Tipos utilitarios
Genéricos que trae TypeScript para transformar tipos: Partial, Pick, Omit, Record…
TypeScript incluye tipos utilitarios que crean un tipo nuevo a partir de otro. Partial<T> hace opcionales todas las propiedades; Pick<T, "a" | "b"> se queda solo con algunas; Omit<T, "id"> quita las que indiques; Record<K, V> describe un objeto con claves de tipo K y valores de tipo V; y ReturnType<typeof f> extrae el tipo que devuelve una función.
La ventaja es que el tipo derivado sigue al original: si mañana añades stock a Producto, Omit<Producto, "id"> lo incluye sin que toques nada. Hay más, como Required, Readonly, Parameters o Awaited.
Úsalo cuando
Para los datos de un formulario de edición (Partial), lo que envías al crear algo (Omit del id), vistas reducidas (Pick), diccionarios (Record) y para tipar lo que devuelve una función de otra librería (ReturnType).
Ten en cuenta
Omit no avisa si escribes mal la propiedad: Omit<Producto, "ide"> compila sin quejarse y no quita nada. Y encadenar muchos utilitarios produce mensajes de error difíciles de leer; dale un nombre al resultado con type.
interface Producto {
id: number;
nombre: string;
precio: number;
}
type NuevoProducto = Omit<Producto, "id">; // nombre y precio
type CambiosProducto = Partial<NuevoProducto>; // todo opcional
type Resumen = Pick<Producto, "nombre" | "precio">;
type StockPorTienda = Record<string, number>; // tienda → unidades
function crearCarrito() {
return { lineas: [] as Producto[], total: 0 };
}
type Carrito = ReturnType<typeof crearCarrito>;
// { lineas: Producto[]; total: number }Configurar y compilar
TypeScript hace dos trabajos que conviene no confundir: comprobar los tipos y convertir el código en JavaScript. Hoy es habitual que los hagan herramientas diferentes.
tsconfig.json y el modo strict
El archivo que dice qué se comprueba y con qué reglas.
El tsconfig.json de la raíz del proyecto configura el compilador: qué archivos incluir (include), a qué versión de JavaScript apuntar (target), qué sistema de módulos usar (module) y cómo de estrictas son las comprobaciones. Los frameworks generan uno razonable al crear el proyecto; esta misma app tiene el suyo.
La opción más importante es "strict": true. Activa un grupo de comprobaciones, entre ellas strictNullChecks, que te obliga a tener en cuenta null y undefined, y noImplicitAny, que no deja que un parámetro sin tipo se convierta en any en silencio. Sin strict, TypeScript deja pasar muchos de los errores que justifican usarlo.
Úsalo cuando
Activa strict desde el primer día en cualquier proyecto nuevo. Para migrar un proyecto de JavaScript, allowJs te deja mezclar archivos .js y .ts mientras conviertes poco a poco.
Ten en cuenta
El tsconfig.json admite comentarios, aunque parezca JSON. Y activar strict en un proyecto grande que ya existe puede destapar cientos de errores de golpe: hazlo por fases.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true, // solo comprobar: otra herramienta genera el JS
"skipLibCheck": true
},
"include": ["src"]
}tsc y la eliminación de tipos
tsc comprueba y compila; otras herramientas solo borran los tipos, mucho más rápido.
tsc es el compilador oficial: comprueba los tipos de todo el proyecto y, si se lo pides, genera los archivos .js. Es exhaustivo, pero lento en proyectos grandes, porque necesita analizar todos los archivos juntos. Con tsc --noEmit solo comprueba, sin generar nada.
Muchas herramientas modernas separan los dos trabajos. esbuild, SWC, bundlers como Vite y, en sus versiones recientes, el propio Node.js hacen type stripping (eliminación de tipos): borran las anotaciones y ejecutan o empaquetan el JavaScript que queda, sin comprobar nada. Por eso son casi instantáneas. La comprobación queda para el editor y para tsc --noEmit en el pipeline de CI.
Úsalo cuando
Usa la eliminación de tipos para desarrollar y ejecutar rápido, y tsc --noEmit antes de integrar cambios. Genera .js con tsc cuando publicas una librería y necesitas también sus archivos de tipos .d.ts.
Ten en cuenta
Borrar tipos solo funciona con sintaxis que se puede borrar: los enum o las propiedades de parámetro en los constructores generan código, así que Node.js no los admite por defecto. La opción erasableSyntaxOnly del tsconfig.json te avisa si los usas. Y una herramienta que solo borra tipos ejecutará tu código aunque tenga errores de tipos.
# Comprobar los tipos de todo el proyecto, sin generar archivos
npx tsc --noEmit
# En versiones recientes de Node.js: ejecuta el .ts borrando los tipos
node servidor.ts
# En una librería (sin noEmit): genera los .js y los .d.ts
npx tsc¿Sabías que…?
En 2025, Microsoft anunció que estaba portando el compilador de TypeScript a Go para que comprobar proyectos grandes fuera muchas veces más rápido. El lenguaje es el mismo: lo que cambia es la herramienta que lo comprueba.
Interactivo
Tres formas de convertir TypeScript en JavaScript
Activa o desactiva opciones para compararlas cara a cara.
| Característica | tsc | Node.js (type stripping) | esbuild, SWC y bundlers |
|---|---|---|---|
| Comprueba los tipos | Sí, de todo el proyecto | No | No |
| Velocidad | La más lenta | Casi instantánea | Muy rápida |
| Sintaxis que admite | Toda la de TypeScript | Solo la que se puede borrar: sin enum por defecto | Casi toda; procesan cada archivo por separado |
| Genera archivos .d.ts | Sí | No | No por sí solos |
| Uso típico | Comprobar en CI y publicar librerías | Ejecutar scripts y servidores .ts | Desarrollar y empaquetar aplicaciones |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Descubres decenas de parámetros sin tipo que TypeScript trata como any sin avisar, y quieres que el compilador impida que aparezcan más.
Para recordar
- Deja que TypeScript infiera los tipos locales y anota las fronteras: parámetros, funciones públicas y datos externos, estos con
unknown. interfaceytypeson casi intercambiables para objetos; solotypenombra uniones y solointerfacese fusiona.- Las uniones discriminadas, junto con el narrowing, modelan estados sin combinaciones imposibles.
- Los genéricos y los tipos utilitarios derivan tipos nuevos de los que ya tienes, en lugar de copiarlos.
- Activa
strict; ejecuta con herramientas que borran tipos y comprueba contsc --noEmiten el CI.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Dónde conviene escribir anotaciones de tipo?
2.Con
mascota: Gato | Perro, escribesif ("ladrar" in mascota) { … }. ¿Qué sabe TypeScript dentro delif?3.Con
type Respuesta = { estado: "ok"; datos: string[] } | { estado: "error"; mensaje: string }, ¿qué te permite leerr.datossin error?4.Ejecutas
node app.tscon una versión reciente de Node.js y funciona, aunque el archivo tiene un error de tipos. ¿Por qué?5.¿Por qué
unknownes más seguro queanypara el resultado deJSON.parse?