learning-front

Nivel 5 · TypeScript: JavaScript con red de seguridad

Genéricos

Escribir funciones y tipos reutilizables que conservan la información de tipo, con constraints, inferencia y valores por defecto.

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:

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

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

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

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

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

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