Lección 7 de 8. Los tipos avanzados en TypeScript son casi un lenguaje dentro del lenguaje: tienen bucles (tipos mapeados), condiciones (tipos condicionales) y hasta plantillas de texto. Aquí verás cómo están hechos los utilitarios de la lección anterior y dos herramientas que sí usarás a diario: as const y satisfies.
Antes de empezar, una advertencia honesta: esta lección es la que menos vas a escribir en el día a día. Su utilidad principal es leer los tipos de las bibliotecas sin asustarte y, de vez en cuando, ahorrarte cien líneas de tipos repetidos. Si algo no encaja a la primera, sigue: la lección 8 no depende de esta.
Tipos mapeados: un bucle sobre las claves
¿Cómo está hecho Partial, de la lección 6? Es un tipo mapeado: recorre las claves de un tipo con [K in keyof T] y, para cada una, escribe una propiedad nueva. Se pueden escribir en una línea:
type Opcional<T> = { [K in keyof T]?: T[K] }; // lo mismo que Partial<T>
type SoloLectura<T> = { readonly [K in keyof T]: T[K] }; // lo mismo que Readonly<T>
type Obligatorio<T> = { [K in keyof T]-?: T[K] }; // -? quita el opcional: Required<T>
interface Producto { nombre: string; precio: number; stock?: number }
const borrador: Opcional<Producto> = { nombre: "Taza" };
const fijo: SoloLectura<Producto> = { nombre: "Vaso", precio: 3 };
const completo: Obligatorio<Producto> = { nombre: "Jarra", precio: 9, stock: 4 };
fijo.precio = 4; // error: readonly
const incompleto: Obligatorio<Producto> = { nombre: "Plato", precio: 5 }; // error: falta stockÁbrelo en el editor y cámbialo →
tsc --strictmapeado.ts(11,6): error TS2540: No se puede asignar a "precio" porque es una propiedad de solo lectura.
mapeado.ts(12,7): error TS2741: La propiedad "stock" falta en el tipo "{ nombre: string; precio: number; }", pero es obligatoria en el tipo "Obligatorio<Producto>".Con as se puede además renombrar cada clave. Combinado con los tipos de plantilla que verás más abajo, permite fabricar, por ejemplo, un getter por cada propiedad:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Persona { nombre: string; edad: number }
const p: Getters<Persona> = {
getNombre: () => "Lucía",
getEdad: () => 34,
};
console.log(p.getNombre(), p.getEdad() + 1);Ábrelo en el editor y cámbialo →
salidaLucía 35
Tipos condicionales: un if para tipos
Un tipo condicional elige entre dos tipos según si uno encaja en otro: A extends B ? X : Y se lee «si A cabe en B, X; si no, Y». Cuando A es una unión, se aplica a cada miembro por separado, y eso lo hace muy útil para filtrar uniones:
type SinNulos<T> = T extends null | undefined ? never : T; // lo mismo que NonNullable<T> type Entrada = string | number | null | undefined; type Limpia = SinNulos<Entrada>; // string | number const ok: Limpia = 42; const mal: Limpia = null; // error
Ábrelo en el editor y cámbialo →
tsc --strictcondicional.ts(7,7): error TS2322: El tipo 'null' no se puede asignar al tipo 'Limpia'.
El truco está en never, el conjunto vacío de la lección 2: en una unión, never desaparece, así que «devolver never» equivale a quitar ese miembro.
infer: sacar un tipo de dentro de otro
Dentro de la condición, infer declara un tipo que TypeScript debe deducir. Así se extrae el tipo de los elementos de un array o lo que hay dentro de una promesa; ReturnType y Awaited están hechos así:
type ElementoDe<T> = T extends (infer U)[] ? U : never; type Desenvuelto<T> = T extends Promise<infer U> ? U : T; type A = ElementoDe<string[]>; // string type B = Desenvuelto<Promise<number[]>>; // number[] const a: A = "texto"; const b: B = [1, 2, 3]; const c: A = 5; // error: A es string
Ábrelo en el editor y cámbialo →
tsc --strictinfer.ts(9,7): error TS2322: El tipo 'number' no se puede asignar al tipo 'string'.
Tipos de plantilla de texto
Las plantillas de JavaScript (`Hola, ${nombre}`) tienen su versión en tipos. Con uniones dentro, generan todas las combinaciones, y con ${number} admiten cualquier número en esa posición:
type Lado = "top" | "bottom";
type Borde = `border-${Lado}`; // "border-top" | "border-bottom"
type Medida = `${number}px` | `${number}%`;
function estilo(propiedad: Borde, valor: Medida): string {
return `${propiedad}: ${valor};`;
}
console.log(estilo("border-top", "2px"));
console.log(estilo("border-bottom", "50%"));
estilo("border-left", "2px"); // error
estilo("border-top", "2em"); // errorÁbrelo en el editor y cámbialo →
tsc --strictplantillas.ts(11,8): error TS2345: No se puede asignar un argumento de tipo ""border-left"" al parámetro de tipo ""border-bottom" | "border-top"". plantillas.ts(12,22): error TS2345: No se puede asignar un argumento de tipo ""2em"" al parámetro de tipo "Medida".
El error que te ahorra: escuchar un evento con el nombre mal escrito
import { EventEmitter } from "node:events";
const conexion = new EventEmitter();
conexion.on("conectdo", () => console.log("¡Conectado!")); // errata
conexion.emit("conectado");
console.log("fin del programa");node eventos_antes.mjsfin del programa
No hay error, solo silencio: el aviso nunca se muestra, porque nadie escucha el evento que se emite. Es uno de los fallos más difíciles de encontrar. Con un mapa de eventos y keyof, el nombre del evento deja de ser un texto cualquiera:
type Eventos = {
conectado: () => void;
mensaje: (texto: string) => void;
};
class Emisor {
#oyentes = new Map<keyof Eventos, Array<(...args: never[]) => void>>();
on<K extends keyof Eventos>(evento: K, oyente: Eventos[K]): void {
this.#oyentes.set(evento, [...(this.#oyentes.get(evento) ?? []), oyente]);
}
}
const conexion = new Emisor();
conexion.on("conectdo", () => console.log("¡Conectado!"));
conexion.on("mensaje", (texto) => console.log(texto.toUpperCase())); // texto es stringtsc --stricteventos.ts(15,13): error TS2345: No se puede asignar un argumento de tipo ""conectdo"" al parámetro de tipo "keyof Eventos".
Por eso el addEventListener del navegador te avisa si escribes "clik": sus tipos usan exactamente este patrón. Lo verás en el ejemplo Tipar eventos del DOM.
as const: valores que no se ensanchan
En la lección 2 viste que let ensancha "oscuro" a string. Con los arrays y los objetos pasa siempre, aunque uses const, porque su contenido sí puede cambiar. as const lo impide: todo queda de solo lectura y con sus valores literales. Su uso estrella es definir la lista una sola vez y sacar de ella el tipo:
const ROLES = ["admin", "editor", "lector"] as const;
type Rol = (typeof ROLES)[number]; // "admin" | "editor" | "lector"
function puedeEditar(rol: Rol): boolean {
return rol !== "lector";
}
for (const r of ROLES) console.log(r, puedeEditar(r));
puedeEditar("invitado"); // errorÁbrelo en el editor y cámbialo →
tsc --strictas_const.ts(9,13): error TS2345: No se puede asignar un argumento de tipo ""invitado"" al parámetro de tipo ""admin" | "editor" | "lector"".
Si mañana añades un rol al array, el tipo Rol lo incluye solo, y además tienes la lista para recorrerla en ejecución, cosa que un tipo no permite. Es la alternativa habitual a los enum, que Node no sabe ejecutar (lección 1); hay un ejemplo completo en Alternativas a enum.
satisfies: comprobar sin perder detalle
Anotar una variable tiene un coste: su tipo pasa a ser exactamente el de la anotación, y se pierde lo que TypeScript sabía de más. satisfies, de TypeScript 4.9, comprueba que el valor cumple un tipo pero conserva el tipo deducido:
type Config = Record<string, string | number>;
const anotada: Config = { host: "localhost", puerto: 8080 };
const comprobada = { host: "localhost", puerto: 8080 } satisfies Config;
console.log(comprobada.puerto.toFixed(0)); // puerto es number
console.log(anotada.puerto.toFixed(0)); // error: para Config, puede ser string
const mala = { host: "localhost", puerto: true } satisfies Config; // errorÁbrelo en el editor y cámbialo →
tsc --strictsatisfies.ts(7,28): error TS2339: La propiedad 'toFixed' no existe en el tipo 'string | number'. La propiedad 'toFixed' no existe en el tipo 'string'. satisfies.ts(8,35): error TS2322: El tipo 'boolean' no se puede asignar al tipo 'string | number'.
Con la anotación, anotada.puerto es string | number, porque así lo dice Config; con satisfies, el compilador comprueba igual que todo cumple Config (la última línea falla), pero recuerda que puerto es un número.
Resumen
| Herramienta | Forma | Para |
|---|---|---|
| Tipo mapeado | { [K in keyof T]: ... } | transformar todas las propiedades |
| Renombrar claves | [K in keyof T as ...] | getters, eventos, prefijos |
| Tipo condicional | A extends B ? X : Y | elegir o filtrar tipos |
| Deducir dentro | infer U | sacar el tipo de un array o una promesa |
| Plantilla de texto | `border-${Lado}` | textos con formato fijo |
| No ensanchar | as const | derivar uniones de una lista |
| Comprobar sin anotar | satisfies T | conservar el tipo exacto |
Retos
Reto 1. Escribe un tipo mapeado Anulable<T> que permita null en todas las propiedades de T, y úsalo con un formulario de dos campos.
Ver la solución
type Anulable<T> = { [K in keyof T]: T[K] | null };
interface Formulario { nombre: string; edad: number }
const vacio: Anulable<Formulario> = { nombre: null, edad: null };
const medio: Anulable<Formulario> = { nombre: "Ana", edad: null };
console.log(vacio, medio);Ábrelo en el editor y cámbialo →
salida{ nombre: null, edad: null } { nombre: 'Ana', edad: null }Reto 2. Escribe type Parametros<F> que, dada una función, dé la tupla de sus parámetros (es lo que hace el utilitario Parameters). Pista: infer.
Ver la solución
type Parametros<F> = F extends (...args: infer A) => unknown ? A : never;
function mover(x: number, y: number, suave?: boolean) {}
type P = Parametros<typeof mover>; // [x: number, y: number, suave?: boolean]
const ok: P = [10, 20];
const mal: P = [10, "20"]; // errorÁbrelo en el editor y cámbialo →
tsc --strictreto2.ts(7,21): error TS2322: El tipo 'string' no se puede asignar al tipo 'number'.
Reto 3. Con as const, define una tabla COLORES de nombres a códigos hexadecimales y deriva de ella el tipo NombreColor con las claves. Usa satisfies para comprobar que todos los valores empiezan por #.
Ver la solución
const COLORES = {
acento: "#1e64af",
tinta: "#0f1f33",
borde: "#e3e8ee",
} as const satisfies Record<string, `#${string}`>;
type NombreColor = keyof typeof COLORES; // "acento" | "tinta" | "borde"
function color(n: NombreColor): string {
return COLORES[n];
}
console.log(color("acento"), color("borde"));Ábrelo en el editor y cámbialo →
salida#1e64af #e3e8ee
Si un valor no empezara por #, satisfies lo marcaría; y as const mantiene los códigos exactos en vez de ensancharlos a string.
Todo el código de esta lección se comprobó con tsc 7.0.2 en modo estricto (--strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; los mensajes de error y las salidas están copiados de esa ejecución. ¿Quieres probar algo? Usa el editor de TypeScript.
