🎯 Meta: dominar TypeScript, el lenguaje que usan dos de tus stacks (NestJS y Elysia) y gran parte del backend moderno. Aquí tienes lo que necesitas para no perderte con los tipos.
Versión: TypeScript 7.0 (GA desde el 8 de julio de 2026) — el compilador nativo en Go.
A.1 · ¿Qué es TypeScript y por qué usarlo en backend?
TypeScript es JavaScript + tipos. Escribes JS normal, pero declaras qué tipo tiene cada cosa, y el compilador detecta errores antes de ejecutar.
// JavaScript: este bug explota en producción
function precioConIgv(precio) {
return precio * 1.18;
}
precioConIgv("100"); // "1001.181.18..." 😱 (concatena texto)
// TypeScript: el error se ve al ESCRIBIR, no en producción
function precioConIgv(precio: number): number {
return precio * 1.18;
}
precioConIgv("100"); // ❌ Error: 'string' no es asignable a 'number'🧠 Por qué importa en backend: un backend maneja dinero, permisos y datos de usuarios. Un
undefineddonde esperabas un número puede corromper una factura o abrir un agujero de seguridad. TypeScript convierte esos errores en avisos del editor, no en incidentes de producción. Por eso es el estándar en backend serio de 2026.
A.2 · Tipos básicos
// Primitivos
let nombre: string = "Ana";
let edad: number = 29;
let activo: boolean = true;
let nada: null = null;
let indefinido: undefined = undefined;
// Arrays
let numeros: number[] = [1, 2, 3];
let nombres: Array<string> = ["Ana", "Beto"];
// Tuplas (array de longitud y tipos fijos)
let coordenada: [number, number] = [10, 20];
// any (evítalo: apaga TypeScript) vs unknown (seguro: obliga a comprobar)
let peligroso: any = "lo que sea"; // ❌ sin comprobación
let seguro: unknown = obtenerDato(); // ✅ hay que verificar el tipo antes de usar
// Inferencia: TypeScript deduce el tipo solo (no hace falta declararlo)
let ciudad = "Lima"; // TS sabe que es string⚠️
anyes la puerta de atrás que anula TypeScript. Cadaanyes un trozo donde vuelves a JavaScript sin red. Úsalo solo como último recurso. Si no conoces el tipo, usaunknown(te obliga a comprobarlo antes de usarlo). Configura"noImplicitAny": truepara que te avise.
A.3 · Interfaces y types (la clave del backend)
Describen la "forma" de un objeto — es como defines tus datos y DTOs:
// interface: la forma de un objeto
interface Usuario {
id: number;
nombre: string;
email: string;
edad?: number; // ? = opcional
readonly creadoEn: Date; // readonly = no se puede cambiar tras crear
}
const u: Usuario = {
id: 1,
nombre: "Ana",
email: "ana@x.com",
creadoEn: new Date(),
};
// type: similar, pero más flexible (uniones, intersecciones)
type ID = number | string; // unión: puede ser uno u otro
type Estado = "activo" | "inactivo" | "baneado"; // unión de literales (¡muy útil!)
type Admin = Usuario & { permisos: string[] }; // intersección: combina tipos🧠
interfacevstype: para objetos, casi intercambiables. Regla práctica: usainterfacepara formas de objetos que podrían extenderse; usatypepara uniones, primitivos y combinaciones. En la práctica, elige uno y sé coherente.
Uniones de literales — el patrón que más usarás en backend (estados, roles, métodos):
type MetodoHttp = "GET" | "POST" | "PUT" | "DELETE";
function manejar(metodo: MetodoHttp) { /* ... */ }
manejar("GET"); // ✅
manejar("get"); // ❌ Error: no es un MetodoHttp válido — ¡el typo se atrapa solo!A.4 · Funciones tipadas
// Parámetros y retorno tipados
function sumar(a: number, b: number): number {
return a + b;
}
// Parámetros opcionales y por defecto
function saludar(nombre: string, saludo: string = "Hola"): string {
return `${saludo}, ${nombre}`;
}
// Funciones flecha
const doble = (n: number): number => n * 2;
// Funciones async (devuelven Promise)
async function obtenerUsuario(id: number): Promise<Usuario> {
const resp = await fetch(`/api/usuarios/${id}`);
return resp.json();
}A.5 · Genéricos (reutilización con tipos)
Un genérico es un "tipo variable": escribes código que funciona con cualquier tipo sin perder la seguridad. Es lo que hace que un repositorio sirva para cualquier entidad:
// Sin genéricos: una función por tipo (repetitivo)
// Con genéricos: una función para todos, tipada
function primero<T>(lista: T[]): T | undefined {
return lista[0];
}
primero<number>([1, 2, 3]); // devuelve number
primero(["a", "b"]); // TS infiere string
// Genéricos en interfaces — el patrón repositorio tipado:
interface Repositorio<T> {
buscar(id: number): Promise<T | null>;
guardar(entidad: T): Promise<T>;
listar(): Promise<T[]>;
}
class UsuarioRepositorio implements Repositorio<Usuario> {
async buscar(id: number): Promise<Usuario | null> { /* ... */ }
async guardar(u: Usuario): Promise<Usuario> { /* ... */ }
async listar(): Promise<Usuario[]> { /* ... */ }
}🧠
<T>da miedo al principio, pero es simple: es un hueco que se rellena con un tipo concreto al usarlo.Repositorio<Usuario>es "un repositorio de usuarios". El mismo código sirve paraRepositorio<Producto>. Sin genéricos tendrías que copiar la interfaz para cada entidad.
A.6 · Tipos de utilidad (los que ahorran horas)
TypeScript trae "transformadores" de tipos incorporados. Estos son los que usarás en backend:
interface Usuario {
id: number;
nombre: string;
email: string;
password: string;
}
// Partial<T>: todos los campos opcionales (para updates PATCH)
type ActualizarUsuario = Partial<Usuario>; // { id?, nombre?, email?, password? }
// Omit<T, K>: quita campos (nunca devuelvas el password)
type UsuarioPublico = Omit<Usuario, "password">; // sin password
// Pick<T, K>: selecciona solo algunos campos
type Credenciales = Pick<Usuario, "email" | "password">;
// Required<T>: todos obligatorios; Readonly<T>: todos inmutables
type UsuarioCompleto = Required<Usuario>;💡 Tip de oro:
Omit<Usuario, "password">es la forma tipada de garantizar que nunca filtras la contraseña en una respuesta. El compilador te lo impide. Es seguridad por tipos — conecta con "separar modelo de entrada y salida" que viste en FastAPI (cap. 05) y NestJS (cap. 06).
A.7 · Tipos avanzados: satisfies, condicionales y mapped types
Con generics y utility types (A.5-A.6) cubres el 90% del backend. El 10% restante —el que se ve en librerías y APIs bien tipadas— usa estas tres piezas:
// satisfies: valida contra un tipo SIN ensanchar el tipo inferido (a diferencia de `: Tipo` o `as Tipo`)
type Ruta = { path: string; metodo: "GET" | "POST" };
const rutas = {
listar: { path: "/productos", metodo: "GET" },
crear: { path: "/productos", metodo: "POST" },
} satisfies Record<string, Ruta>;
rutas.listar.metodo; // TS sabe que es "GET" exacto (con `: Record<string, Ruta>` sería "GET"|"POST")🧠
satisfiesvs:Tipovsas Tipo:: Tipoensancha cada propiedad al tipo declarado (pierdes precisión);as Tipono comprueba nada (puedes mentirle al compilador);satisfiescomprueba que encaje en el tipo Y conserva el tipo más preciso que escribiste. Es la opción correcta casi siempre que antes usabasas.
Uniones discriminadas + exhaustividad — el patrón para modelar estados que no se pueden mezclar (y que el compilador te avise si olvidas un caso):
type EstadoPedido =
| { tipo: "pendiente" }
| { tipo: "pagado"; fechaPago: Date }
| { tipo: "cancelado"; motivo: string };
function describir(estado: EstadoPedido): string {
switch (estado.tipo) {
case "pendiente": return "Esperando pago";
case "pagado": return `Pagado el ${estado.fechaPago}`; // TS SABE que existe fechaPago aquí
case "cancelado": return `Cancelado: ${estado.motivo}`;
default:
const _exhaustivo: never = estado; // ❌ error de compilación si añades un tipo y olvidas el case
return _exhaustivo;
}
}💡 El truco de
neveren eldefault: si mañana añades{ tipo: "reembolsado" }a la unión y olvidas sucase, eldefaultya NO recibenever(recibe el caso que falta) y el compilador marca error. Es una red de seguridad gratis contra "olvidé un estado nuevo".
Tipos condicionales y mapped types — cómo están hechos por dentro Partial, Omit, etc. (no los escribirás a diario, pero te permiten leer los tipos de librerías):
// Condicional: el tipo "if/else" de TypeScript
type EsArray<T> = T extends unknown[] ? true : false;
type A = EsArray<string[]>; // true
type B = EsArray<string>; // false
// Mapped type: transforma cada propiedad de un tipo (así está hecho Partial<T> internamente)
type SoloLectura<T> = { readonly [K in keyof T]: T[K] };
// Template literal types: strings validados por forma
type RutaAPI = `/api/${string}`;
const ruta: RutaAPI = "/api/productos"; // ✅
// const mala: RutaAPI = "/productos"; // ❌ no empieza por /api/🔗 Cuando definas tipos de eventos o rutas dinámicas en NestJS/Elysia (caps. 06-07), reconocer estos patrones te ayuda a leer los tipos de las librerías (Zod, tRPC, Elysia) sin que te intimiden — casi todas se apoyan en condicionales y mapped types por debajo.
A.8 · Narrowing (estrechar tipos) y null safety
TypeScript te obliga a manejar los casos null/undefined, evitando el error nº1 de JS ("cannot read property of undefined"):
function procesar(usuario: Usuario | null) {
// ❌ Error: usuario podría ser null
// console.log(usuario.nombre);
// ✅ Comprueba primero (narrowing)
if (usuario === null) {
return;
}
console.log(usuario.nombre); // aquí TS SABE que no es null
// Operadores útiles:
const nombre = usuario?.nombre ?? "Anónimo"; // ?. seguro, ?? valor por defecto
}⚠️ Activa
"strict": trueen tutsconfig.json. EnciendestrictNullChecksy compañía, que son la razón de ser de TypeScript. Sin modo estricto, TS es un JavaScript con adornos. Todos los proyectos serios usanstrict.
A.9 · tsconfig.json recomendado (backend 2026)
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true, // ← lo más importante
"noImplicitAny": true,
"noUncheckedIndexedAccess": true, // array[i] puede ser undefined (más seguro)
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
}
}A.10 · TypeScript 7: qué cambia (y qué NO)
TypeScript 7.0 salió estable el 8 de julio de 2026. Es el compilador nativo reescrito en Go (antes estaba escrito en el propio TypeScript). Lo que necesitas saber:
Qué cambia (para bien):
| Novedad | Impacto |
|---|---|
| 8-12× más rápido | El type-checking de un proyecto grande pasa de minutos a segundos |
| Menor uso de memoria | ~50% menos RAM en proyectos grandes |
| Language server más estable | Menos cuelgues del editor (VS Code responde al instante) |
| Multithreading | Aprovecha varios núcleos para chequear tipos |
Ejemplo real citado: Slack redujo el type-check en CI de ~7,5 min a ~1 min.
Qué NO cambia (tranquilo):
- Es un port, no un rewrite: la semántica de tipos es idéntica. Tu código TypeScript funciona igual. No tienes que reaprender nada.
- La sintaxis, los tipos, los genéricos, todo lo de este apéndice sigue igual.
Cómo usarlo:
npm install -D typescript@7 # instala TS 7
npx tsc --version # Version 7.0.x
npx tsc # compila (¡mucho más rápido!)🧠 Nomenclatura confusa (aclaración): el compilador clásico (escrito en TypeScript) siguió la serie 5.x → 6.x; el nuevo compilador nativo en Go se llama 7.0. Ambos dan el mismo resultado; el 7 es "el mismo TypeScript, pero corriendo a velocidad de Go". Para tus proyectos NestJS (cap. 06) y Elysia (cap. 07): actualiza a TS 7 y disfruta builds instantáneos, sin tocar tu código.
💡 Dato curioso que cierra el círculo del libro: el compilador de TypeScript ahora está escrito en Go (cap. 08). El lenguaje "de infraestructura" que aprendiste al final resultó ser la base del lenguaje que usaste en NestJS y Elysia. Todo conecta.
A.11 · Buenas prácticas TypeScript
"strict": truesiempre. Es el 80% del valor de TypeScript.- Evita
any. Usaunknowny estrecha el tipo, o define el tipo correcto. - Tipa las fronteras: las respuestas de APIs externas y
req.bodyllegan comounknownoany— valídalas (con Zod, class-validator o los esquemas de Elysia) y tipa desde ahí. - Usa
Omit/Pick/Partialpara derivar tipos en vez de duplicarlos. - Uniones de literales para estados y roles (
type Rol = "admin" | "user"), no strings sueltos. - Actualiza a TS 7 para builds rápidos; no requiere cambios de código.
- Valida en runtime lo que viene de fuera: TypeScript solo existe en compilación; en runtime los datos externos pueden ser cualquier cosa. Por eso NestJS y Elysia validan con esquemas.
⚠️ El malentendido nº1 de los novatos con TS: creer que TypeScript valida datos en runtime. NO lo hace. Los tipos desaparecen al compilar. Si tu API recibe
{ edad: "abc" }, TypeScript no lo detecta en ejecución — para eso están los validadores (Zod, class-validator, esquemast.*de Elysia). TypeScript protege tu código; los validadores protegen de datos externos. Ambos son necesarios.
✅ Ejercicio del apéndice
1. Crea una interface Producto y deriva con Omit un tipo ProductoPublico sin
el campo "costo_interno".
2. Escribe un Repositorio<T> genérico e impleméntalo para Producto.
3. Usa una unión de literales para el estado de un pedido
("pendiente" | "pagado" | "enviado" | "cancelado") y una función que solo
acepte transiciones válidas.
4. Activa "strict": true y arregla todos los errores de null que aparezcan.
5. Actualiza uno de tus proyectos (NestJS o Elysia) a TypeScript 7 y compara
el tiempo de compilación antes y después.Con esto lees y escribes el TypeScript de los capítulos 06 y 07 con soltura.
💡 Pistas
- El
Repositorio<T>genérico: la firma de cada método usaT, noProducto— así el mismointerfacesirve paraRepositorio<Producto>yRepositorio<Usuario>sin tocar una línea. - Para la función de transiciones válidas del punto 3, un
Record<Estado, Estado[]>con los estados destino permitidos por cada estado de origen es más mantenible que una cadena deif. - Al activar
strict, sinoUncheckedIndexedAccesste llena de errores en accesos a arrays, es la señal correcta: esos accesos SÍ pueden devolverundefineden runtime.
🧠 Autoevaluación
JavaScript no tiene tipos: "100" * 1.18 se ejecuta igualmente concatenando/coaccionando de forma silenciosa y produce un resultado incorrecto en producción. TypeScript declara precio: number y rechaza la llamada en tiempo de compilación, antes de que el bug llegue a ejecutarse.
any desactiva por completo la comprobación de tipos — puedes llamar cualquier método sobre él sin que el compilador proteste, aunque no exista. unknown es igual de "no sé qué tipo es", pero el compilador te obliga a comprobarlo (narrowing) antes de usarlo — es la versión seria de "no lo sé todavía".
Porque si mañana añades un campo sensible nuevo a Usuario (ej. tokenRecuperacion), el tipo derivado con Omit sigue excluyendo solo lo que le dijiste explícitamente — pero si construyes el objeto de respuesta a mano copiando campos, es fácil olvidar excluir el campo nuevo y filtrarlo por accidente. El compilador no puede protegerte de un error de copiar y pegar; sí puede protegerte de una derivación de tipos.
TypeScript solo existe en tiempo de compilación — los tipos desaparecen al generar el JavaScript final. Los datos que llegan de fuera (req.body, respuestas de APIs externas) no pasan por ningún chequeo de tipos en runtime. Hace falta un validador (Zod, class-validator, esquemas de Elysia) que compruebe la forma real de los datos cuando el programa ya está corriendo.
Es una comprobación de exhaustividad: si el switch cubre todos los casos posibles de la unión, lo que llega al default es de tipo never y compila. Si mañana añades un nuevo miembro a la unión y olvidas su case, ese valor ya no es asignable a never y el compilador marca error en ese punto exacto — te avisa en tiempo de compilación de que dejaste un caso sin manejar.
as Tipo es una afirmación sin comprobación: le dices al compilador "confía en mí", incluso si el objeto no encaja de verdad. satisfies sí comprueba que el objeto cumpla el tipo, y además conserva el tipo más específico de cada propiedad (en vez de ensancharlo al tipo declarado) — obtienes validación y precisión al mismo tiempo, lo que as nunca te da.
Porque es un port del compilador a Go, no una reescritura del lenguaje: la semántica de tipos es idéntica a la de las versiones anteriores. Lo único que cambia es la velocidad de compilación (8-12× más rápido) y el consumo de memoria — el código TypeScript que ya escribiste sigue significando exactamente lo mismo.
Volver al: README.md · Relacionado: 06-node-nestjs.md, 07-bun-elysia.md