Los tipos genéricos son una de las características más potentes de TypeScript y también una de las más infrautilizadas. Si escribes any cuando no sabes qué tipo usar, necesitas leer esto. Los genéricos son la solución correcta a ese problema.

¿Qué son los tipos genéricos?

Los genéricos te permiten crear componentes que trabajan con una variedad de tipos en lugar de un solo tipo. Son como variables para los tipos. El código genérico es reutilizable y a la vez seguro en cuanto a tipos.

// Sin genéricos: tienes que duplicar código o usar any
function identidadNumber(arg: number): number {
  return arg;
}

function identidadString(arg: string): string {
  return arg;
}

// Con genéricos: una función que funciona con cualquier tipo
function identidad<T>(arg: T): T {
  return arg;
}

// TypeScript infiere el tipo automáticamente
const num = identidad(42);        // T es number
const str = identidad("hola");    // T es string

// O puedes especificarlo explícitamente
const explicito = identidad<boolean>(true);

Genéricos con interfaces y tipos

Puedes usar genéricos en interfaces y tipos para crear estructuras de datos flexibles y reutilizables:

// Respuesta genérica de API
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

// Usar con distintos tipos de datos
type UserResponse = ApiResponse<User>;
type ProductsResponse = ApiResponse<Product[]>;
type PaginatedResponse<T> = ApiResponse<{
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}>;

// Estado genérico para componentes React
interface EstadoAsync<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

const estadoInicial: EstadoAsync<User> = {
  data: null,
  loading: false,
  error: null
};

Restricciones con extends

A veces necesitas que el tipo genérico tenga ciertas propiedades. Para eso usas extends en la declaración del genérico:

// El tipo T debe tener la propiedad length
function obtenerLongitud<T extends { length: number }>(arg: T): number {
  return arg.length;
}

obtenerLongitud("hola");       // ✅ string tiene length
obtenerLongitud([1, 2, 3]);    // ✅ array tiene length
obtenerLongitud(42);           // ❌ Error: number no tiene length

// El tipo T debe ser una clave de U
function obtenerPropiedad<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const usuario = { nombre: "Ana", edad: 28, email: "ana@ejemplo.com" };
const nombre = obtenerPropiedad(usuario, "nombre");  // string
const edad = obtenerPropiedad(usuario, "edad");      // number
// obtenerPropiedad(usuario, "telefono");            // ❌ Error

Utility Types: los genéricos que ya vienen incluidos

TypeScript incluye una colección de tipos de utilidad genéricos que son increíblemente útiles en el día a día:

interface Usuario {
  id: number;
  nombre: string;
  email: string;
  password: string;
  fechaNacimiento: Date;
}

// Partial: hace todas las propiedades opcionales
type ActualizarUsuario = Partial<Usuario>;
// { id?: number; nombre?: string; email?: string; ... }

// Required: hace todas las propiedades obligatorias
type UsuarioCompleto = Required<Usuario>;

// Pick: selecciona solo algunas propiedades
type UsuarioPublico = Pick<Usuario, "id" | "nombre" | "email">;

// Omit: excluye propiedades
type UsuarioSinPassword = Omit<Usuario, "password">;

// Readonly: hace todas las propiedades de solo lectura
type UsuarioCongelado = Readonly<Usuario>;

// Record: crea un tipo de objeto con claves y valores específicos
type RolePermisos = Record<"admin" | "editor" | "viewer", string[]>;

// ReturnType: extrae el tipo de retorno de una función
function crearUsuario() { return { id: 1, nombre: "Ana" }; }
type TipoUsuarioCreado = ReturnType<typeof crearUsuario>;
// { id: number; nombre: string }

Tipos condicionales

Los tipos condicionales son como el operador ternario pero para tipos. Te permiten crear lógica de tipos realmente compleja:

// Tipo condicional básico
type EsString<T> = T extends string ? "sí es string" : "no es string";

type Test1 = EsString<string>;  // "sí es string"
type Test2 = EsString<number>;  // "no es string"

// NonNullable: elimina null y undefined
type NonNullable<T> = T extends null | undefined ? never : T;

type SoloString = NonNullable<string | null | undefined>; // string

// Extraer el tipo de los elementos de un array
type ElementoDeArray<T> = T extends (infer U)[] ? U : never;

type Elemento = ElementoDeArray<string[]>; // string
type Elemento2 = ElementoDeArray<number[]>; // number

Genéricos en clases

Las clases también pueden ser genéricas, lo que permite crear estructuras de datos tipadas como pilas, colas o repositorios:

class Pila<T> {
  private elementos: T[] = [];

  push(elemento: T): void {
    this.elementos.push(elemento);
  }

  pop(): T | undefined {
    return this.elementos.pop();
  }

  peek(): T | undefined {
    return this.elementos[this.elementos.length - 1];
  }

  get tamaño(): number {
    return this.elementos.length;
  }
}

// TypeScript infiere el tipo automáticamente
const pilaNumeros = new Pila<number>();
pilaNumeros.push(1);
pilaNumeros.push(2);
const ultimo = pilaNumeros.pop(); // number | undefined

// No permite tipos incorrectos
// pilaNumeros.push("texto"); // ❌ Error

// Repositorio genérico para cualquier entidad
class Repositorio<T extends { id: number }> {
  private items: Map<number, T> = new Map();

  guardar(item: T): T {
    this.items.set(item.id, item);
    return item;
  }

  buscarPorId(id: number): T | undefined {
    return this.items.get(id);
  }

  obtenerTodos(): T[] {
    return Array.from(this.items.values());
  }
}

Patrones avanzados: Builder y Factory genéricos

// Factory genérico
function crear<T>(constructor: new (...args: unknown[]) => T, ...args: unknown[]): T {
  return new constructor(...args);
}

// Combinar múltiples tipos (Intersection types)
type ConTimestamps<T> = T & {
  creadoEn: Date;
  actualizadoEn: Date;
};

type UsuarioConTimestamps = ConTimestamps<Usuario>;

// Mapear tipos para transformarlos
type HacerOpcional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UsuarioConEmailOpcional = HacerOpcional<Usuario, "email">;

Dominar los genéricos de TypeScript te permite escribir código que es a la vez flexible y completamente tipado. Deja de usar any y empieza a pensar en términos de tipos genéricos. Tu equipo, y tu yo futuro, te lo agradecerán.