CodeForge

TypeScript de Cero a Experto / Genéricos

Genéricos desde cero: código que se adapta

Teoría22 min20 XP

Aquí empieza la magia mayor de TypeScript. Un genérico es una función o tipo que funciona con CUALQUIER tipo sin renunciar a la seguridad —recibe un número y sabe que devuelve un número; recibe un Gasto y sabe que devuelve un Gasto. Es lo que hace reutilizable tu código sin caer en any. Suena abstracto; en una función de tres líneas lo verás clarísimo.

El molde que se adapta al contenido

El problema: reutilizar sin perder el tipo

Imagina una función que devuelve el primer elemento de un array. Sin genéricos, tienes dos opciones malas:

el-problema.ts
// opción mala 1: una función por tipo (repetición infinita)
function primerNumero(arr: number[]): number { return arr[0]; }
function primerTexto(arr: string[]): string { return arr[0]; }
// ...¿y para Gasto? ¿y para Usuario? insostenible

// opción mala 2: any (funciona con todo, pero BORRA el tipo)
function primeroAny(arr: any[]): any { return arr[0]; }
const x = primeroAny([1, 2, 3]);   // x es 'any' → perdiste que era number
x.toUpperCase();                    // TS no se queja... 💥 revienta en runtime

La opción 1 te obliga a escribir la misma función para cada tipo. La opción 2 funciona con todos, pero devuelve any —y ya sabes (módulo 2) que any apaga la seguridad: el resultado pierde su tipo y TS deja de protegerte. Necesitas algo que funcione con cualquier tipo Y lo conserve. Eso es un genérico.

Tu primer genérico: identidad<T>

Un genérico introduce una VARIABLE DE TIPO —por convención T— entre <>. Es un "tipo por determinar" que TS rellena según cómo uses la función:

identidad.ts
// <T> declara una variable de tipo; T se usa como cualquier tipo
function identidad<T>(valor: T): T {
return valor;
}
// se lee: "para algún tipo T, recibe un T y devuelve un T"

// TS INFIERE T según lo que pases —no tienes que escribirlo:
const a = identidad("hola");   // T = string → a es string
const b = identidad(42);       // T = number → b es number
const c = identidad(true);     // T = boolean → c es boolean

a.toUpperCase();   // ✓ TS sabe que a es string
b.toFixed(2);      // ✓ TS sabe que b es number
// b.toUpperCase(); // ❌ TS lo marca: b es number, no string

Compara con any: identidad(42) devuelve number (no any), así que TS sigue protegiéndote. La <T> es la etiqueta de la máquina: se adapta a lo que entra y lo recuerda a la salida. Y lo mejor —no escribes identidad<string>("hola"): TS INFIERE que T es string por el argumento. Los genéricos casi siempre se infieren solos.

El primero de un array, genérico

El caso real del principio, ahora resuelto de una vez para todos los tipos:

primero.ts
// funciona con arrays de CUALQUIER tipo, y conserva el tipo del elemento:
function primero<T>(arr: T[]): T | undefined {
return arr[0];   // undefined si el array está vacío (strict, módulo 1)
}

const n = primero([10, 20, 30]);          // T = number → n es number | undefined
const s = primero(["a", "b"]);            // T = string → s es string | undefined
const g = primero([{ nombre: "Café" }]);  // T = { nombre: string } → g tipado

// una sola función, todos los tipos, cero any, seguridad total:
if (n !== undefined) console.log(n.toFixed(2));

¿Cuál es la ventaja de function primero<T>(arr: T[]): T frente a function primero(arr: any[]): any?

Mini-reto

Genera tu primer genérico útil: 1) escribe function pares<T>(a: T, b: T): [T, T] que devuelva una tupla con ambos valores del MISMO tipo; 2) pruébala con dos números y con dos strings, y confirma que TS rechaza pares(1, "a") (tipos distintos para el mismo T); 3) escribe function repetir<T>(valor: T, veces: number): T[] que devuelva un array con valor repetido. Sientes cómo un solo T ata los tipos entre parámetros y retorno.

Qué sigue

Un genérico simple ya es poderoso. La próxima lección sube la apuesta: varios parámetros de tipo a la vez (<T, U>), para funciones que relacionan DOS tipos —como un map que transforma de un tipo a otro, o pares clave-valor. La caja de herramientas genérica se agranda.