Lección 6 de 8. Los genéricos en TypeScript son tipos que llevan un hueco para otro tipo. Gracias a ellos, map sabe que un array de números da otro array del tipo que devuelvas, y una función de búsqueda devuelve exactamente lo que buscabas, no un any. Aquí aprenderás a escribirlos y a usar los tipos utilitarios que vienen de serie.
De la primera función genérica a todo lo demás
En la lección 4 escribiste primero<T>, una función que sirve para arrays de cualquier tipo y devuelve el tipo correcto en cada llamada. Esa idea, un tipo que es un parámetro, está por todas partes: Array<T>, Promise<T>, Map<K, V> son genéricos que ya has usado sin llamarlos así. Una función puede tener varios parámetros de tipo:
function emparejar<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = emparejar("edad", 42); // [string, number]
const [clave, valor] = p;
console.log(clave.toUpperCase(), valor.toFixed(1));
const indice = new Map<string, number[]>(); // también se pueden escribir a mano
indice.set("pares", [2, 4, 6]);
console.log(indice.get("pares")?.length);Ábrelo en el editor y cámbialo →
salidaEDAD 42.0 3
Restricciones: T, pero no cualquiera
Dentro de una función genérica, T puede ser cualquier cosa, así que no puedes usar nada específico suyo. Si necesitas, por ejemplo, su propiedad length, hay que restringir T con extends: «T puede ser cualquier tipo que tenga un length numérico»:
function masLargo<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(masLargo("casa", "murciélago")); // T = string
console.log(masLargo([1, 2, 3], [4, 5])); // T = number[]
console.log(masLargo(10, 200)); // error: un número no tiene lengthÁbrelo en el editor y cámbialo →
tsc --strictrestriccion.ts(7,22): error TS2345: No se puede asignar un argumento de tipo "number" al parámetro de tipo "{ length: number; }".Es el tipado estructural de la lección 5 otra vez: a la restricción no le importa si es un texto o un array, solo que tenga length.
keyof y el acceso indexado
keyof T es la unión de los nombres de las propiedades de T, y T[K] es el tipo de la propiedad K. Juntos permiten escribir una función que lee una propiedad de un objeto y devuelve el tipo exacto de esa propiedad, y que no acepta claves que no existen:
interface Usuario {
id: number;
nombre: string;
email: string;
}
function obtener<T, K extends keyof T>(obj: T, clave: K): T[K] {
return obj[clave];
}
const u: Usuario = { id: 1, nombre: "Ana", email: "ana@ejemplo.com" };
const id = obtener(u, "id"); // number
const nombre = obtener(u, "nombre"); // string
console.log(id + 1, nombre.toUpperCase());
type Email = Usuario["email"]; // string: el tipo de una propiedad
obtener(u, "nombr"); // errorÁbrelo en el editor y cámbialo →
tsc --strictkeyof.ts(17,12): error TS2345: No se puede asignar un argumento de tipo ""nombr"" al parámetro de tipo "keyof Usuario".
Interfaces, tipos y clases genéricos
Los parámetros de tipo no son solo para funciones. Un tipo genérico describe una forma que se repite con contenidos distintos, como la respuesta de una API que a veces trae usuarios y a veces pedidos. Y pueden tener un valor por omisión, como los parámetros de una función:
type Respuesta<T> =
| { ok: true; datos: T }
| { ok: false; error: string };
interface Pagina<T = string> {
elementos: T[];
total: number;
}
function mostrar(r: Respuesta<number[]>) {
if (r.ok) console.log("suma:", r.datos.reduce((a, b) => a + b, 0));
else console.log("error:", r.error);
}
mostrar({ ok: true, datos: [3, 4, 5] });
mostrar({ ok: false, error: "tiempo agotado" });
const nombres: Pagina = { elementos: ["Ana", "Luis"], total: 2 }; // T = string
console.log(nombres.elementos.join(", "));Ábrelo en el editor y cámbialo →
salidasuma: 12 error: tiempo agotado Ana, Luis
Fíjate en que Respuesta<T> es la unión discriminada de la lección 3, ahora reutilizable para cualquier tipo de datos. Las clases funcionan igual: el parámetro de tipo se fija al crear el objeto.
class Pila<T> {
#elementos: T[] = [];
apilar(x: T): void { this.#elementos.push(x); }
desapilar(): T | undefined { return this.#elementos.pop(); }
get tamano(): number { return this.#elementos.length; }
}
const deshacer = new Pila<string>();
deshacer.apilar("escribir 'hola'");
deshacer.apilar("borrar línea");
console.log(deshacer.desapilar(), "| quedan", deshacer.tamano);
deshacer.apilar(42); // error: la pila es de textosÁbrelo en el editor y cámbialo →
tsc --strictpila.ts(13,17): error TS2345: No se puede asignar un argumento de tipo "number" al parámetro de tipo "string".
Los tipos utilitarios
TypeScript trae de serie unos cuantos tipos genéricos que fabrican tipos nuevos a partir de otros. No hace falta memorizarlos todos; estos son los que se ven en cualquier proyecto:
| Utilitario | Qué hace | Para qué se usa |
|---|---|---|
Partial<T> | todas las propiedades opcionales | los cambios de un «actualizar» |
Required<T> | todas obligatorias | una configuración ya completada |
Readonly<T> | todas de solo lectura | estado que no se debe modificar |
Pick<T, K> | solo las propiedades K | lo que se muestra en una lista |
Omit<T, K> | todas menos K | un usuario sin la contraseña |
Record<K, V> | un objeto con claves K y valores V | diccionarios y tablas de búsqueda |
ReturnType<F> | el tipo que devuelve la función F | reutilizar el tipo de algo que no exportan |
El error que te ahorra: una errata en el nombre de un campo al actualizar
function actualizar(usuario, cambios) {
return { ...usuario, ...cambios };
}
const u = { id: 1, nombre: "Ana", email: "ana@ejemplo.com" };
const nuevo = actualizar(u, { emial: "ana@empresa.com" });
console.log(nuevo.email);node actualizar_antes.mjsana@ejemplo.com
No hay error: el objeto tiene ahora un campo emial nuevo y el email de siempre, sin cambiar. El usuario sigue recibiendo los correos en la dirección antigua y nadie sabe por qué. Con Partial<Usuario>, los cambios solo pueden ser campos de Usuario:
interface Usuario { id: number; nombre: string; email: string }
function actualizar(usuario: Usuario, cambios: Partial<Usuario>): Usuario {
return { ...usuario, ...cambios };
}
const u: Usuario = { id: 1, nombre: "Ana", email: "ana@ejemplo.com" };
const nuevo = actualizar(u, { emial: "ana@empresa.com" });tsc --strictactualizar.ts(8,31): error TS2561: El literal de objeto solo puede especificar propiedades conocidas, pero "emial" no existe en el tipo "Partial<Usuario>". ¿Quería escribir "email"?
Y así se ven los demás en acción:
interface Usuario {
id: number;
nombre: string;
email: string;
clave: string;
}
type UsuarioPublico = Omit<Usuario, "clave">;
type Fila = Pick<Usuario, "id" | "nombre">;
type Rol = "admin" | "editor" | "lector";
const permisos: Record<Rol, string[]> = {
admin: ["leer", "escribir", "borrar"],
editor: ["leer", "escribir"],
lector: ["leer"],
};
function crearUsuario(nombre: string) {
return { id: Date.now(), nombre, activo: true };
}
type Creado = ReturnType<typeof crearUsuario>; // { id: number; nombre: string; activo: boolean }
const publico: UsuarioPublico = { id: 1, nombre: "Ana", email: "ana@ejemplo.com" };
const fila: Fila = { id: publico.id, nombre: publico.nombre };
const c: Creado = { id: 2, nombre: "Luis", activo: false };
console.log(fila, permisos.editor, c.activo);Ábrelo en el editor y cámbialo →
salida{ id: 1, nombre: 'Ana' } [ 'leer', 'escribir' ] falseDos detalles. En Record<Rol, string[]>, si olvidas uno de los tres roles, el compilador se queja: un Record con claves de una unión exige todas. Y typeof crearUsuario dentro de un tipo no es el typeof de JavaScript, sino el de TypeScript: «el tipo de esta variable». Cómo se construyen estos utilitarios por dentro (son unas pocas líneas cada uno) es el tema de la lección 7.
Promesas: el genérico de cada día
Una función async siempre devuelve una promesa, y su tipo es Promise<T>, donde T es lo que obtienes con await. Las promesas y async/await están explicados en la lección de errores y asincronía de JavaScript; aquí solo cambia la anotación:
interface Tarea { id: number; hecha: boolean }
function esperar(ms: number): Promise<void> {
return new Promise((resolver) => setTimeout(resolver, ms));
}
async function cargarTareas(): Promise<Tarea[]> {
await esperar(50); // simula una petición a un servidor
return [{ id: 1, hecha: true }, { id: 2, hecha: false }];
}
const tareas = await cargarTareas(); // Tarea[]
console.log(tareas.filter((t) => !t.hecha).length, "pendiente(s)");Ábrelo en el editor y cámbialo →
salida1 pendiente(s)
T una sola vez (por ejemplo, function f<T>(x: T): void), el genérico no aporta nada: unknown haría lo mismo y se lee mejor.Resumen
| Para | Escribe | Recuerda |
|---|---|---|
| Una función para cualquier tipo | f<T>(x: T): T | TypeScript deduce T |
| Que T tenga algo | T extends { length: number } | basta con la forma |
| Las claves de un tipo | keyof T | una unión de textos literales |
| El tipo de una propiedad | T[K], Usuario["email"] | — |
| Un tipo con hueco | type Respuesta<T> = ... | admite valor por omisión |
| Derivar tipos | Partial, Pick, Omit, Record | no copies tipos a mano |
| Una función asíncrona | : Promise<T> | await da T |
Retos
Reto 1. Escribe agrupar<T, K extends keyof T>(lista: T[], clave: K) que devuelva un Map del valor de esa clave a los elementos que lo tienen. Pruébala agrupando productos por categoría.
Ver la solución
function agrupar<T, K extends keyof T>(lista: T[], clave: K): Map<T[K], T[]> {
const grupos = new Map<T[K], T[]>();
for (const x of lista) {
const k = x[clave];
grupos.set(k, [...(grupos.get(k) ?? []), x]);
}
return grupos;
}
const productos = [
{ nombre: "Taza", categoria: "cocina" },
{ nombre: "Lámpara", categoria: "salón" },
{ nombre: "Sartén", categoria: "cocina" },
];
for (const [cat, ps] of agrupar(productos, "categoria")) {
console.log(cat, ps.map((p) => p.nombre));
}Ábrelo en el editor y cámbialo →
salidacocina [ 'Taza', 'Sartén' ] salón [ 'Lámpara' ]
Reto 2. Define un tipo NuevoUsuario a partir de Usuario (con id, nombre, email y creado: Date) sin el id ni la fecha, que los pone el servidor, y con el email opcional. Pista: combina Omit, Partial, Pick y &.
Ver la solución
interface Usuario { id: number; nombre: string; email: string; creado: Date }
type NuevoUsuario = Omit<Usuario, "id" | "creado" | "email"> & Partial<Pick<Usuario, "email">>;
const a: NuevoUsuario = { nombre: "Ana" };
const b: NuevoUsuario = { nombre: "Luis", email: "luis@ejemplo.com" };
const c: NuevoUsuario = { nombre: "Eva", id: 3 }; // error: el id no se permiteÁbrelo en el editor y cámbialo →
tsc --strictreto2.ts(7,42): error TS2353: El literal de objeto solo puede especificar propiedades conocidas y 'id' no existe en el tipo 'NuevoUsuario'.
Reto 3. Esta función genérica no compila. ¿Por qué, y cómo se arregla? function sumarTodo<T>(lista: T[]): number { return lista.reduce((a, b) => a + b, 0); }
Ver la solución
function sumarTodo<T>(lista: T[]): number {
return lista.reduce((a, b) => a + b, 0);
}Ábrelo en el editor y cámbialo →
tsc --strictreto3.ts(2,33): error TS2365: El operador '+' no se puede aplicar a los tipos 'number' y 'T'.
Sin restricción, T podría ser cualquier cosa, y no se puede sumar cualquier cosa. Aquí no hace falta un genérico: basta con lista: number[]. Es el caso de la nota de arriba: si T no sirve para nada, sobra.
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.
