Skip to content

🎯 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.

typescript
// 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 undefined donde 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

typescript
// 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

⚠️ any es la puerta de atrás que anula TypeScript. Cada any es un trozo donde vuelves a JavaScript sin red. Úsalo solo como último recurso. Si no conoces el tipo, usa unknown (te obliga a comprobarlo antes de usarlo). Configura "noImplicitAny": true para 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:

typescript
// 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

🧠 interface vs type: para objetos, casi intercambiables. Regla práctica: usa interface para formas de objetos que podrían extenderse; usa type para 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):

typescript
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

typescript
// 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:

typescript
// 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 para Repositorio<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:

typescript
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:

typescript
// 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")

🧠 satisfies vs :Tipo vs as Tipo: : Tipo ensancha cada propiedad al tipo declarado (pierdes precisión); as Tipo no comprueba nada (puedes mentirle al compilador); satisfies comprueba que encaje en el tipo Y conserva el tipo más preciso que escribiste. Es la opción correcta casi siempre que antes usabas as.

Uniones discriminadas + exhaustividad — el patrón para modelar estados que no se pueden mezclar (y que el compilador te avise si olvidas un caso):

typescript
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 never en el default: si mañana añades { tipo: "reembolsado" } a la unión y olvidas su case, el default ya NO recibe never (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):

typescript
// 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"):

typescript
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": true en tu tsconfig.json. Enciende strictNullChecks y compañía, que son la razón de ser de TypeScript. Sin modo estricto, TS es un JavaScript con adornos. Todos los proyectos serios usan strict.


A.9 · tsconfig.json recomendado (backend 2026)

jsonc
{
  "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):

NovedadImpacto
8-12× más rápidoEl 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 estableMenos cuelgues del editor (VS Code responde al instante)
MultithreadingAprovecha 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:

bash
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

  1. "strict": true siempre. Es el 80% del valor de TypeScript.
  2. Evita any. Usa unknown y estrecha el tipo, o define el tipo correcto.
  3. Tipa las fronteras: las respuestas de APIs externas y req.body llegan como unknown o any — valídalas (con Zod, class-validator o los esquemas de Elysia) y tipa desde ahí.
  4. Usa Omit/Pick/Partial para derivar tipos en vez de duplicarlos.
  5. Uniones de literales para estados y roles (type Rol = "admin" | "user"), no strings sueltos.
  6. Actualiza a TS 7 para builds rápidos; no requiere cambios de código.
  7. 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, esquemas t.* 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 usa T, no Producto — así el mismo interface sirve para Repositorio<Producto> y Repositorio<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 de if.
  • Al activar strict, si noUncheckedIndexedAccess te llena de errores en accesos a arrays, es la señal correcta: esos accesos SÍ pueden devolver undefined en 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