Hasta ahora has tipado valores concretos: un Heroe, un string, un number[].
Pero a veces necesitas escribir una función que funcione igual de bien con un
Heroe[] que con un number[], y que en los dos casos el compilador recuerde
exactamente qué tipo maneja. Ahí entran los genéricos.
El problema: perder el tipo con any#
Imagina que quieres una función que devuelva el primer elemento de cualquier
array. El primer intento suele ser usar any:
// any: acepta cualquier array, pero a cambio olvida qué hay dentro.
function primero(lista: any[]): any {
return lista[0];
}
// TypeScript no sabe que h es un Heroe: no avisa si accedemos mal.
const h = primero(equipo);
// Sin error en tiempo de compilación, aunque ese campo no exista: any apaga el chequeo.
console.log(h.campoQueNoExiste);any “soluciona” el problema del tipo a cambio de apagar la red de seguridad.
El editor ya no puede ayudarte: pierdes el autocompletado, no ves errores donde
los hay, y los fallos aparecen en tiempo de ejecución (cuando ya es tarde).
Existe otra opción que a veces se intenta: usar object. A diferencia de any,
object no apaga el chequeo — simplemente acepta cualquier valor no primitivo
(objetos, arrays, funciones). El problema es que pierde la forma concreta: TypeScript
solo sabe que es “algún objeto”, no que es un Heroe con nombre, rol y victorias.
Por eso los accesos a esos campos siguen dando error. No es tan peligroso como any,
pero tampoco resuelve el problema real: la información de tipo se pierde igual.
La función genérica <T>#
La solución es un parámetro de tipo: un hueco con un nombre (por convención
T) que TypeScript rellena en cada llamada con el tipo real del argumento.
// <T> es el parámetro de tipo: un hueco entre el nombre y los paréntesis.
// La función dice: "dame un array de T, y te devuelvo un T o undefined".
function primero<T>(lista: T[]): T | undefined {
return lista[0];
}Ahora, cuando llamas a primero(equipo) con un Heroe[], TypeScript sabe que
el retorno es Heroe | undefined. No any, no object: el tipo exacto.
El parámetro de tipo#
T es el nombre habitual para el parámetro de tipo, pero puedes llamarlo como
quieras (Item, Elemento, TDato). Lo que importa es dónde va:
// Nombre de función parámetro de tipo parámetros normales retorno
// | | | |
function primero <T> (lista: T[]) : T | undefined {
return lista[0];
}El parámetro de tipo va entre el nombre de la función y el paréntesis de apertura. TypeScript lo usará como una variable que contiene un tipo, no un valor. No existe en tiempo de ejecución: desaparece al compilar igual que el resto de las anotaciones.
Inferencia del genérico#
No hace falta escribir <Heroe> en cada llamada. TypeScript mira el tipo del
argumento y rellena T por sí solo:
// Forma explícita: funciona, pero es redundante.
const a = primero<Heroe>(equipo);
// Forma inferida: TypeScript ve que "equipo" es Heroe[] y deduce T = Heroe.
const b = primero(equipo);Ambas líneas producen exactamente el mismo tipo (Heroe | undefined). La
versión inferida es la habitual: escribes menos y el compilador trabaja más.
Constraints con extends#
A veces la función necesita acceder a algún campo del objeto. Sin restricción,
TypeScript no puede permitirlo: T podría ser cualquier cosa y quizá no tiene
ese campo.
Una constraint (restricción) acota T para que cumpla una forma mínima:
// <T extends { victorias: number }> exige que T tenga al menos ese campo.
// T puede tener más campos: la constraint es un mínimo, no un exacto.
function rankear<T extends { victorias: number }>(lista: T[]): T[] {
return [...lista].sort(function(a, b) {
// Aquí TypeScript sabe que a y b tienen .victorias: sin error.
return b.victorias - a.victorias;
});
}El extends de una constraint no es lo mismo que el extends de interface:
no crea herencia de clases. Le dice a TypeScript “T puede ser cualquier tipo,
siempre que tenga esta forma mínima”.
Valor por defecto <T = ...>#
Un parámetro de tipo puede tener un valor por defecto, igual que un parámetro
de función. Si no se especifica T al usar el tipo, TypeScript aplica el
default:
// Si no se escribe el tipo entre <>, TypeScript asume Heroe.
type Resultado<T = Heroe> = {
operacion: string;
total: number;
datos: T[];
};
// Aquí T toma el default (Heroe): resultado es Resultado<Heroe>.
const resultado: Resultado = { operacion: "listar", total: 2, datos: equipo };
// Aquí se pasa T explícito: resultado es Resultado<number>.
const puntuaciones: Resultado<number> = { operacion: "puntos", total: 3, datos: [10, 20, 30] };El valor por defecto es útil en tipos genéricos que tienen un caso habitual dominante. Así los usos comunes son breves y los casos especiales siguen siendo posibles.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué hace el parámetro de tipo `<T>` en una función genérica?
Tu turno#
Tienes dos funciones que usan object y pierden el tipo al devolverlo. Tu
tarea: conviértelas en genéricas para que TypeScript conserve el tipo exacto de
entrada a salida. El panel “Problemas” y los TODO te guían. Cuando lo tengas (o
si te atascas), despliega las soluciones y fíjate en el salto de un nivel al
siguiente.
Ejercicio · en esta página
Utilidades genéricas para el Team Builder
Tienes dos funciones que usan "object" y pierden el tipo. Tu tarea: conviértelas en genéricas para que TypeScript conserve el tipo exacto de entrada a salida. El panel "Problemas" y los TODO te guían.
Paso 1: Que funcione
- El panel "Problemas" queda vacío: no hay errores de tipo.
- Las dos funciones tienen parámetros de tipo: `<T>` en primero y `<T extends ...>` en rankear.
- Al pulsar "Ejecutar" se ve el ranking en la consola.
Paso 2: Que sea limpio
- No escribes `<Heroe>` de forma explícita en ninguna llamada: TypeScript infiere T solo.
- Las firmas de las funciones son claras: el retorno de `primero` es `T | undefined`, no `T`.
- Sin `any` ni `@ts-ignore` en ningún sitio.
Paso 3: Que sea reutilizable
- Añades un tipo genérico `Resultado<T>` que envuelve los datos con metadatos (operacion, total, datos).
- El tipo de retorno de `rankear` es `Resultado<T>`, no `T[]`: el tipo exacto se conserva.
- Todo type-chequea limpio y sin `any`: la seguridad de tipos queda intacta de principio a fin.
Ver soluciones
// SOLUCIÓN OK — el genérico funciona y el panel "Problemas" queda vacío.
// El punto débil: se anota <Heroe> de forma explícita en cada llamada
// en vez de dejar que TypeScript lo deduzca.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
{ nombre: "Ana", rol: "Apoyo", partidas: 140, victorias: 98 },
];
// <T> convierte la función en genérica: T se rellena en cada llamada.
function primero<T>(lista: T[]): T | undefined {
return lista[0];
}
// La constraint <T extends { victorias: number }> obliga a que los objetos de la lista tengan "victorias".
// Es precisamente esa constraint la que permite acceder a a.victorias y b.victorias dentro del sort:
// sin ella, TypeScript rechazaría ese acceso porque T podría ser cualquier cosa.
function rankear<T extends { victorias: number }>(lista: T[]): T[] {
return [...lista].sort(function (a, b) {
// a y b tienen .victorias gracias a la constraint: TypeScript lo sabe y no da error.
return b.victorias - a.victorias;
});
}
// Se pasa <Heroe> de forma explícita en cada llamada.
// Funciona, pero es ruido: TypeScript ya sabe que equipo es Heroe[]
// y puede deducir T = Heroe sin que se lo digamos. Ver tier "mejor".
const primeroDelEquipo = primero<Heroe>(equipo);
console.log(
"Primero: " + (primeroDelEquipo ? primeroDelEquipo.nombre : "ninguno"),
);
// Igual que antes: <Heroe> explícito aquí es redundante porque TypeScript lo infiere.
const ranking = rankear<Heroe>(equipo);
ranking.forEach(function (h, i) {
console.log(i + 1 + ". " + h.nombre + " — " + h.victorias + " victorias");
}); Por qué este nivel
- Añade `<T>` y la constraint correctas en ambas funciones: el panel queda vacío y el código funciona.
- Escribe `<Heroe>` de forma explícita en cada llamada: funciona, pero es redundante. TypeScript ya lo sabe por el argumento.
- Es la traducción directa de los TODO: correcto, pero hay margen de pulido.
// SOLUCIÓN MEJOR — TypeScript infiere T a partir del argumento.
// No hace falta escribir <Heroe> en cada llamada: el tipo se deduce solo.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
{ nombre: "Ana", rol: "Apoyo", partidas: 140, victorias: 98 },
];
// Igual que antes, pero la inferencia se encarga de rellenar T.
function primero<T>(lista: T[]): T | undefined {
return lista[0];
}
// La constraint <T extends { victorias: number }> acota T: cualquier objeto con al menos ese campo vale.
// Sin la constraint, TypeScript no permitiría acceder a a.victorias ni b.victorias dentro del sort.
function rankear<T extends { victorias: number }>(lista: T[]): T[] {
return [...lista].sort(function (a, b) {
// La constraint garantiza que a y b tienen .victorias: por eso TypeScript no da error aquí.
return b.victorias - a.victorias;
});
}
// TypeScript ve que lista es Heroe[], deduce T = Heroe.
// primeroDelEquipo tiene tipo Heroe | undefined, no object.
const primeroDelEquipo = primero(equipo);
console.log(
"Primero: " + (primeroDelEquipo ? primeroDelEquipo.nombre : "ninguno"),
);
// TypeScript deduce T = Heroe; el array de salida también es Heroe[].
const ranking = rankear(equipo);
ranking.forEach(function (h, i) {
console.log(i + 1 + ". " + h.nombre + " — " + h.victorias + " victorias");
}); Por qué es mejor que el anterior
- Elimina los `<Heroe>` explícitos en las llamadas: TypeScript infiere T a partir del argumento, así que son ruido.
- La inferencia hace el trabajo: el tipo de retorno es igualmente preciso sin escribirlo.
- El código queda más limpio y sigue siendo igual de seguro.
// SOLUCIÓN EXCELENTE — API genérica completa que conserva el tipo EXACTO.
// El tipo de salida no se ensancha a un supertipo: si entra Heroe[], sale Heroe[].
// Además se añade un tipo genérico Resultado<T> para envolver el retorno con metadatos.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
{ nombre: "Ana", rol: "Apoyo", partidas: 140, victorias: 98 },
];
// Tipo genérico: un contenedor reutilizable para cualquier operación.
// T = Heroe es el valor por defecto: si no se escribe el tipo entre <>,
// TypeScript asume Resultado<Heroe>, que es el caso más habitual en este contexto.
// Para cualquier otro tipo basta con escribir Resultado<number>, Resultado<string>, etc.
type Resultado<T = Heroe> = {
// La operación que generó este resultado.
operacion: string;
// El número de elementos procesados.
total: number;
// Los datos con su tipo exacto conservado.
datos: T[];
};
// primero<T>: devuelve el primer elemento conservando el tipo exacto.
// La inferencia lo rellena: si entra T[], devuelve T | undefined.
function primero<T>(lista: T[]): T | undefined {
return lista[0];
}
// rankear<T>: ordena por victorias conservando el tipo exacto de los objetos.
// La constraint <T extends { victorias: number }> exige que el campo exista.
// Sin la constraint, TypeScript no puede acceder a .victorias dentro del sort.
function rankear<T extends { victorias: number }>(lista: T[]): Resultado<T> {
const datos = [...lista].sort(function (a, b) {
// TypeScript sabe que a y b tienen .victorias porque T lo exige.
return b.victorias - a.victorias;
});
// El objeto de retorno es Resultado<T>: envuelve los datos con metadatos.
return {
operacion: "rankear",
total: datos.length,
datos,
};
}
// TypeScript infiere T = Heroe a partir de "equipo: Heroe[]".
// El tipo de primeroDelEquipo es Heroe | undefined, no object ni unknown.
const primeroDelEquipo = primero(equipo);
if (primeroDelEquipo !== undefined) {
// Aquí TypeScript sabe que primeroDelEquipo es Heroe: accede a .nombre sin error.
console.log("Primero del equipo: " + primeroDelEquipo.nombre);
}
// El tipo de retorno es Resultado<Heroe>, no Resultado<object>.
// ranking.datos es Heroe[], con acceso completo a todos sus campos.
const ranking = rankear(equipo);
console.log("Ranking (" + ranking.total + " heroes):");
ranking.datos.forEach(function (h, i) {
// h es Heroe: TypeScript permite acceder a .nombre, .victorias, .partidas, .rol.
const wr = ((h.victorias / h.partidas) * 100).toFixed(1);
console.log(
i +
1 +
". " +
h.nombre +
" (" +
h.rol +
") — " +
h.victorias +
" victorias (" +
wr +
"%)",
);
}); Por qué es mejor que el anterior
- Introduce `Resultado<T>`: un tipo genérico reutilizable que envuelve cualquier array con metadatos.
- El tipo de retorno de `rankear` es `Resultado<T>`, no `T[]`: si entra `Heroe[]`, sale `Resultado<Heroe>`. El tipo concreto se conserva hasta el final.
- Sin `any`, sin `@ts-ignore`, sin tipo ensanchado a `object`: la red de seguridad queda intacta de principio a fin.