← React & TypeScript

Fundamentos de TypeScript

¿Qué son los generics en TypeScript y para qué sirven?

Ver respuesta — intenta responderla en voz alta primero

Los generics son parámetros de tipo: permiten escribir funciones y estructuras que trabajan con varios tipos sin perder la relación entre la entrada y la salida. Si a primerElemento le pasas un string[], el compilador sabe que devuelve string, no un genérico any.

Imagina una función que devuelve el primer elemento de un arreglo. Sin generics tienes dos malas opciones: tiparla con any (pierdes toda la seguridad) o escribir una versión por cada tipo (duplicas código). Los generics dan la tercera vía:

  • <T> es un parámetro de tipo: como los parámetros normales, pero de tipos. Se "rellena" en cada llamada, casi siempre por inferencia.
  • Conservan la relación entrada → salida: function primero<T>(arr: T[]): T promete que devuelve el mismo tipo que contiene el arreglo.
  • Restricciones con extends: <T extends { id: number }> significa "cualquier tipo, siempre que tenga un id numérico". Dentro de la función puedes usar .id con seguridad.
  • K extends keyof T: el patrón estrella. K solo puede ser una clave real de T, así que acceder a objeto[clave] es seguro y el retorno queda tipado como T[K].

En React los usas sin darte cuenta: useState<number>(0) es un hook genérico.

// Sin generics se pierde el tipo:
// function primerElemento(arr: any[]): any  ->  la salida es any

// Con generics, la relación entrada -> salida se conserva
function primerElemento<T>(arr: T[]): T | undefined {
  return arr[0];
}

const numero = primerElemento([10, 20, 30]);      // number | undefined
const palabra = primerElemento(["a", "b", "c"]);  // string | undefined
// numero.toUpperCase();
// Error: Property 'toUpperCase' does not exist on type 'number'.

// Restricción con extends: T debe tener longitud
function masLargo<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

masLargo("hola", "adiós");     // funciona: string tiene length
masLargo([1, 2], [1, 2, 3]);   // funciona: los arreglos tienen length
// masLargo(10, 20);
// Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

// K extends keyof T: solo acepta claves reales del objeto
function extraerPropiedad<T, K extends keyof T>(objeto: T, clave: K): T[K] {
  return objeto[clave];
}

const usuario = { nombre: "Ana", edad: 30 };
const nombre = extraerPropiedad(usuario, "nombre"); // string
const edad = extraerPropiedad(usuario, "edad");     // number
// extraerPropiedad(usuario, "email");
// Error: Argument of type '"email"' is not assignable to parameter of type '"nombre" | "edad"'.

Usar any donde iba un generic, "porque total, funciona con todo":

// Compila, pero la salida pierde el tipo
function primeroMal(arr: any[]): any {
  return arr[0];
}
const x = primeroMal([1, 2, 3]);
x.toUpperCase(); // compila y explota en runtime: x es any

// Corrección: el generic conserva el tipo
function primeroBien<T>(arr: T[]): T | undefined {
  return arr[0];
}
const y = primeroBien([1, 2, 3]); // number | undefined

La diferencia clave: any acepta todo y olvida todo; el generic acepta todo y recuerda el tipo.

"Un generic es un parámetro de tipo: me deja escribir una sola función que funciona con muchos tipos sin perder la relación entre entrada y salida. Por ejemplo, primerElemento<T>(arr: T[]): T devuelve number si le paso números y string si le paso strings, cosa que con any perdería. Puedo restringir el generic con extends, y el patrón que más uso es K extends keyof T para acceder a propiedades de un objeto de forma segura. En React los uso a diario: useState<T> es un hook genérico."

Reto rápido

Escribe ultimoElemento<T> que devuelva el último elemento de un arreglo (o undefined si está vacío), y comprueba que con ["a", "b"] el resultado queda tipado como string | undefined.

Ver solución
function ultimoElemento<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

const letra = ultimoElemento(["a", "b"]);   // string | undefined
const numero = ultimoElemento([1, 2, 3]);   // number | undefined
const vacio = ultimoElemento<number>([]);   // undefined en runtime

// La relación se conserva: si letra existe, es un string
if (letra !== undefined) {
  console.log(letra.toUpperCase()); // "B"
}