Skip to content

📙 Clase 9 — Genéricos (<T>)

TypeScript · 2026-08-04 · Carpeta: 02-Ejercicios/Genericos ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Qué problema resuelven los genéricos frente a any.
  • Cómo escribir una función genérica con <T>.
  • La diferencia entre fijar el tipo explícitamente (identity<string>(...)) y dejar que TypeScript lo infiera solo.
  • Cómo aplicar genéricos a una clase (Caja<T>), no solo a funciones.
  • Que una vez instanciada con un tipo, la clase queda fija a ese tipo.
  • Que los genéricos también aplican a interfaces, y cómo combinar función + clase + interfaz genéricas en un mismo ejercicio.
  • Cuándo conviene una función, una clase o una interfaz genérica según lo que necesito modelar.

📚 Definiciones clave

TérminoQué esEjemplo de esta clase
Parámetro TEl "hueco" de tipo que declaras entre <> junto al nombre. No guarda un valor, guarda un tipo — se rellena recién cuando usas la función o clase.El T en function identity<T>(arg: T): T
Función genéricaUna función que declara uno o más parámetros de tipo (<T>) y los usa para relacionar sus parámetros con su retorno, funcionando con cualquier tipo sin perder el chequeo.identity<T>, primerElemento<T>
Clase genéricaUna clase que declara <T> junto a su nombre y lo propaga a sus propiedades y métodos, para poder crear instancias de distintos tipos reutilizando la misma definición.class Caja<T>, class Pila<T>
Instancia tipadaEl objeto que resulta de crear (new) una clase genérica fijando su T a un tipo concreto. Queda fija a ese tipo para siempre.new Caja<string>("Libros") → instancia tipada a string

💡 Relación entre los cuatro: el parámetro T es la pieza base; una función genérica o una clase genérica lo usan para declararse flexibles; al crear una instancia tipada de una clase genérica, ese T deja de ser un hueco y se vuelve un tipo concreto y fijo.

Diagrama: de any/unknown a los genéricos, y las 4 piezas de un genérico

📝 Por qué importan los genéricos (explicado simple): a veces necesitas una función que funcione para varios tipos (string, number, lo que sea), sin saber de antemano cuál. La salida fácil sería usar any — pero ahí TypeScript deja de vigilar por completo:

typescript
function identityAny(arg: any): any {
  return arg;
}

let a = identityAny("hola");
a.toFixed(2); // 💥 esto truena al ejecutar, pero TS no avisó nada

Con un genérico (<T>) sigues aceptando cualquier tipo, pero TypeScript se acuerda cuál entró y lo sigue verificando:

typescript
function identity<T>(arg: T): T {
  return arg;
}

let b = identity("hola");
b.toFixed(2); // ❌ TS avisa ANTES de correr: "toFixed no existe en string"

En una frase: genérico = tan flexible como any, pero sin dejar de estar vigilado.


📖 PARTE TEÓRICA

🆚 1. any vs unknown vs genéricos

Los tres aceptan "cualquier valor", pero cada uno afecta la seguridad del código de forma distinta (any/unknown en detalle en Clase 2; el ejemplo de any vs genérico ya lo viste arriba, en el callout de "por qué importan los genéricos"):

TipoAcepta cualquier valor¿Puedes operar con él directo?¿TS sigue vigilando?
any✅ sin restricciones❌ no, apaga el chequeo
unknown❌ solo tras verificar el tipo (typeof)✅ obliga a comprobar antes de usar
Genérico <T>✅ (cualquier tipo, uno por llamada)✅ pero solo con lo que ese tipo permite✅ mantiene la relación entrada→salida

Lo único realmente nuevo de los tres es unknown, que ni any ni un genérico tienen: te obliga a comprobar el tipo antes de dejarte usarlo.

typescript
let valorUnknown: unknown = "hola";
valorUnknown.toUpperCase(); // ❌ TS18046: 'valorUnknown' is of type 'unknown'

if (typeof valorUnknown === "string") {
  valorUnknown.toUpperCase(); // ✅ ya verificado, TS lo permite
}
  • any: el más flexible y el más peligroso — renuncia a la revisión de tipos.
  • unknown: igual de flexible para recibir, pero obliga a comprobar el tipo con typeof antes de usarlo. Es el "any seguro".
  • Genérico <T>: no es "cualquier valor sin revisar" ni "cualquier valor que debo comprobar" — es código reutilizable (funciones, clases) que se adapta al tipo real en cada llamada, manteniendo siempre la relación entre lo que entra y lo que sale.

🧪 Entrevista: unknown vs genérico → ambos son seguros, pero resuelven cosas distintas. unknown es para un valor suelto de tipo desconocido (lo compruebas antes de usarlo). Un genérico es para código reutilizable que debe funcionar igual sea cual sea el tipo, sin perder esa relación de tipos.

🔤 2. Función genérica: identity<T>

typescript
function identity<T>(arg: T): T {
  return arg;
}
  • <T> justo después del nombre declara el parámetro de tipo.
  • arg: T dice "el parámetro es del tipo T".
  • : T (el retorno) dice "devuelvo ese mismo T".

TypeScript relaciona ambos: si entra un string, T se vuelve string para esa llamada, y el retorno también es string.

🧪 Entrevista: ¿Qué gana un genérico sobre any? Que la relación entre entrada y salida se sigue verificando. any apaga la verificación; un genérico solo la hace flexible.

🖼️ Esquema visual — las 3 piezas de una función genérica

El siguiente diagrama es un mnemotécnico propio para recordar qué hace cada parte de identity<T>(arg: T): T (aquí llamada identidade<T>), aplicado a los tres tipos base:

Diagrama de genéricos: las 3 piezas de identity<T>

Transcripción del diagrama (por si la imagen se pierde):

typescript
function identidade<T>(valor: T): T {
  return valor;
}

let output1 = identidade<string>("Hola Mundo");
let output2 = identidade<number>(100);
let output3 = identidade<boolean>(true);
PiezaMnemotécnicoEn criollo
<T>"Papi no sé lo que quiero pero lo quiero"Declaro que habrá un tipo, sin decir todavía cuál es
valor: T"Con gusto mi rey, espero a que me digas lo que quieres"El parámetro acepta el tipo que sea que se le pida
(): T"Y yo te devuelvo lo que esperas sí o sí"El retorno siempre coincide con el tipo que entró

Aplicado a los tres tipos base del diagrama:

Llamada<T>valor: T(): T
identidade<string>("Hola Mundo")quiere un stringespera un stringdevuelve un string
identidade<number>(100)quiere un numberespera un numberdevuelve un number
identidade<boolean>(true)quiere un booleanespera un booleandevuelve un boolean

💡 La idea de fondo es la misma que en el resto de la clase: el <T> que declaras arriba amarra el tipo del parámetro con el tipo del retorno, sea cual sea el tipo que uses en cada llamada.

🎯 3. Tipo explícito vs inferencia

Hay dos formas de usar identity:

typescript
let output1 = identity<string>("Hola"); // tipo explícito: fuerzo T = string
let output2 = identity(42);             // inferencia: TS deduce T = number solo
FormaCómo se escribeCuándo usarla
Explícitaidentity<string>("Hola")Cuando el argumento no deja claro el tipo, o quieres dejarlo documentado
Inferidaidentity(42)Caso normal: TS ya puede deducirlo del argumento

💡 En la práctica, casi siempre se omite <T> y se deja que TypeScript infiera. Escribirlo explícito es útil sobre todo para leer/enseñar, o cuando la inferencia es ambigua.

📦 4. Genéricos en clases: Caja<T>

Los genéricos no son solo para funciones; una clase también puede parametrizarse:

typescript
class Caja<T> {
  private contenido: T;

  constructor(contenido: T) {
    this.contenido = contenido;
  }

  public obtenerContenido(): T {
    return this.contenido;
  }
}
  • class Caja<T> — la clase entera queda parametrizada por T.
  • private contenido: T — la propiedad usa ese mismo tipo.
  • El constructor y obtenerContenido() reutilizan T para mantener todo consistente.
typescript
let cajaDeString = new Caja<string>("Libros");
cajaDeString.obtenerContenido(); // string — TS lo sabe, no hace falta castear

⚠️ Una vez creada new Caja<string>(...), esa instancia queda fija a string. No puedes luego meterle un number — TS lo marca como error.

Si rompes el tipado a propósito para comprobarlo:

typescript
const cajaDeString = new Caja<string>("libros");
cajaDeString.contenido = 42; // ❌ forzado a propósito

TypeScript señala el error exacto (línea y motivo), verificado en terminal:

text
error TS2322: Type 'number' is not assignable to type 'string'.

💡 Ese es el mismo TS2322 que verías con cualquier variable string normal — un genérico no te vuelve más laxo, solo hace que la lógica sea reutilizable para cualquier tipo.

🧬 5. Interfaces genéricas

Las interfaces también aceptan <T>. Sirven para describir la forma de algo sin fijar todavía el tipo concreto — el tipo lo decide quien la implementa o la usa.

typescript
interface Contenedor<T> {
  contenido: T;
  obtenerContenido(): T;
}

class CajaGenerica<T> implements Contenedor<T> {
  contenido: T;

  constructor(valor: T) {
    this.contenido = valor;
  }

  obtenerContenido(): T {
    return this.contenido;
  }
}

function mostrarContenido<T>(caja: Contenedor<T>): T {
  return caja.obtenerContenido();
}

const cajaTexto = new CajaGenerica<string>("Manual de TypeScript");
console.log(mostrarContenido(cajaTexto));   // Manual de TypeScript

const cajaNumero = new CajaGenerica<number>(2026);
console.log(mostrarContenido(cajaNumero));  // 2026

Los tres conceptos trabajando juntos:

  • interface Contenedor<T> — el contrato: "cualquier cosa que implemente esto tiene contenido y obtenerContenido(), del mismo tipo T".
  • class CajaGenerica<T> implements Contenedor<T> — la clase cumple ese contrato con un tipo concreto en cada instancia.
  • function mostrarContenido<T>(caja: Contenedor<T>) — la función no necesita saber qué clase le llega, solo que cumpla la interfaz Contenedor<T>.

🧪 Entrevista: ¿Para qué sirve una interfaz genérica si ya tengo una clase genérica? Para desacoplar: una función que recibe Contenedor<T> funciona con cualquier clase que implemente esa forma, no solo con CajaGenerica.

🎯 6. ¿Cuándo uso función, clase o interfaz genérica?

No son intercambiables — cada una responde a una necesidad distinta:

Necesito...UsoEjemplo de esta clase
Una utilidad reutilizable, sin estadoFunción genéricaidentity<T>, primerElemento<T>
Modelar una entidad con estado (guarda datos)Clase genéricaCaja<T>, Pila<T>
Describir un contrato que varias clases pueden cumplirInterfaz genéricaContenedor<T>

💡 Practicar los tres combinados (como en el ejemplo de Contenedor<T> arriba) deja claro cuándo te conviene cada uno antes de usarlos en un proyecto real. Si rompes el tipado a propósito en cada uno, ves los mensajes de error de TypeScript con calma — y eso enseña más que leerlos solo cuando ya estás bajo presión en un proyecto real.

🧠 7. ¿Por qué esto es mejor que copiar y pegar la clase por cada tipo?

Sin genéricos, tendrías que escribir CajaDeString, CajaDeNumero, CajaDeBoolean... una clase por tipo, todas idénticas salvo el tipo. Con Caja<T> escribes la lógica una sola vez y funciona para cualquier tipo, sin perder el chequeo de tipos.

💡 Ese es el resumen de toda la clase: reutilizar lógica sin duplicar código y sin perder el tipado.


💻 PARTE PRÁCTICA

Archivo trabajado: 02-Ejercicios/Genericos/main.ts

bash
cd 02-Ejercicios/Genericos
tsc && node main.js

Salida real verificada:

zsh · main.ts
$ tsc
$ node main.js
Output 1: Hola
Output 2: 42
Contenido de la caja de string: Libros

Todo verificado y corriendo. ✅

📁 Tip de repo: al crear la carpeta de un ejercicio nuevo, evita tildes en el nombre (Genericos, no Genéricos) — algunas herramientas de Git/GitHub y de compilación siguen dando problemas con acentos en rutas. 02-Ejercicios/Genericos ya sigue esa regla.


🏋️ EJERCICIOS CON SOLUCIÓN

💡 Los 10 van de menor a mayor dificultad: 1-3 son funciones genéricas sueltas, 4-6 meten clases y restricciones, 7-8 combinan interfaces, y 9-10 cierran con lo más avanzado (intersection types y un mini-proyecto integrador).

Ejercicio 1 — identidad<T>

Escríbela de memoria: recibe un valor de cualquier tipo y lo devuelve tal cual, sin perder el tipo. Pruébala con string, number y boolean.

Ver solución
typescript
function identidad<T>(valor: T): T {
  return valor;
}

console.log(identidad<string>("TypeScript"));
console.log(identidad<number>(2026));
console.log(identidad<boolean>(true));

Terminal esperada:

zsh · identidad.ts
$ tsc identidad.ts
$ node identidad.js
TypeScript
2026
true

Ejercicio 2 — ultimoElemento<T>

Función genérica que reciba un array T[] y devuelva su último elemento (no el primero).

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

console.log(ultimoElemento([10, 20, 30]));
console.log(ultimoElemento(["a", "b", "c"]));

Terminal esperada:

zsh · ultimo-elemento.ts
$ tsc ultimo-elemento.ts
$ node ultimo-elemento.js
30
c

Ejercicio 3 — intercambiar<T, U>

Función con dos parámetros de tipo que reciba una tupla [T, U] y devuelva la tupla invertida [U, T].

Ver solución
typescript
function intercambiar<T, U>(par: [T, U]): [U, T] {
  return [par[1], par[0]];
}

console.log(intercambiar(["Styp", 30]));

Terminal esperada:

zsh · intercambiar.ts
$ tsc intercambiar.ts
$ node intercambiar.js
[ 30, 'Styp' ]

Ejercicio 4 — Clase Cola<T> (queue genérico)

Clase genérica con encolar(item: T), desencolar(): T | undefined (saca el primero, no el último — al revés que Pila<T>) y estaVacia(): boolean.

Ver solución
typescript
class Cola<T> {
  private elementos: T[] = [];

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

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

  estaVacia(): boolean {
    return this.elementos.length === 0;
  }
}

const cola = new Cola<string>();
cola.encolar("primero");
cola.encolar("segundo");
cola.encolar("tercero");

console.log(cola.desencolar());
console.log(cola.desencolar());
console.log(cola.estaVacia());

Terminal esperada:

zsh · cola.ts
$ tsc cola.ts
$ node cola.js
primero
segundo
false

💡 Cola (FIFO, shift() saca el primero) vs Pila (LIFO, pop() saca el último) — la única diferencia real está en qué extremo del array se saca.

Ejercicio 5 — Clase Par<T, U>

Clase genérica con dos parámetros de tipo que guarde un par de valores (primero: T, segundo: U) y un método invertir(): Par<U, T>.

Ver solución
typescript
class Par<T, U> {
  constructor(public primero: T, public segundo: U) {}

  invertir(): Par<U, T> {
    return new Par(this.segundo, this.primero);
  }
}

const par = new Par("Styp", 30);
const invertido = par.invertir();
console.log(invertido.primero, invertido.segundo);

Terminal esperada:

zsh · par.ts
$ tsc par.ts
$ node par.js
30 Styp

Ejercicio 6 — Genérico restringido con extends

Función medirTamano<T extends { length: number }> que acepte cualquier tipo con propiedad length (strings y arrays la tienen) y devuelva ese tamaño.

Ver solución
typescript
function medirTamano<T extends { length: number }>(item: T): number {
  return item.length;
}

console.log(medirTamano("hola"));
console.log(medirTamano([1, 2, 3, 4]));

Terminal esperada:

zsh · medir-tamano.ts
$ tsc medir-tamano.ts
$ node medir-tamano.js
4
4

💡 { length: number } no es una interfaz con nombre — es un tipo restrictivo escrito "en línea". Cualquier tipo que tenga esa forma (string, array, y más) califica para T.

Ejercicio 7 — Interfaz genérica + implements

Interfaz Contenedor<T> con agregar(item: T): void y obtenerTodos(): T[]. Clase ListaGenerica<T> implements Contenedor<T> que la cumpla.

Ver solución
typescript
interface Contenedor<T> {
  agregar(item: T): void;
  obtenerTodos(): T[];
}

class ListaGenerica<T> implements Contenedor<T> {
  private items: T[] = [];

  agregar(item: T): void {
    this.items.push(item);
  }

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

const lista = new ListaGenerica<number>();
lista.agregar(1);
lista.agregar(2);
lista.agregar(3);
console.log(lista.obtenerTodos());

Terminal esperada:

zsh · lista-generica.ts
$ tsc lista-generica.ts
$ node lista-generica.js
[ 1, 2, 3 ]

Ejercicio 8 — Comparador<T> + función que lo usa

Interfaz genérica Comparador<T> con comparar(a: T, b: T): number. Una clase ComparadorNumeros implements Comparador<number>. Y una función ordenar<T>(items: T[], comparador: Comparador<T>): T[] que use ese comparador con .sort().

Ver solución
typescript
interface Comparador<T> {
  comparar(a: T, b: T): number;
}

class ComparadorNumeros implements Comparador<number> {
  comparar(a: number, b: number): number {
    return a - b;
  }
}

function ordenar<T>(items: T[], comparador: Comparador<T>): T[] {
  return [...items].sort((a, b) => comparador.comparar(a, b));
}

console.log(ordenar([5, 2, 8, 1], new ComparadorNumeros()));

Terminal esperada:

zsh · ordenar.ts
$ tsc ordenar.ts
$ node ordenar.js
[ 1, 2, 5, 8 ]

💡 ordenar no sabe cómo se comparan los valores — eso se lo delega a cualquier Comparador<T> que le pases. Es el mismo patrón de desacople del Ejercicio 7.

Ejercicio 9 — fusionar<T, U> (intersection types)

Función que reciba dos objetos de tipos distintos (T y U) y devuelva uno solo que tenga las propiedades de ambos (tipo de retorno: T & U, un intersection type — nuevo: "y" en vez de "o" como con |).

Ver solución
typescript
function fusionar<T, U>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const persona = fusionar({ nombre: "Ana" }, { edad: 25 });
console.log(persona.nombre, persona.edad);

Terminal esperada:

zsh · fusionar.ts
$ tsc fusionar.ts
$ node fusionar.js
Ana 25

💡 T & U (intersection) exige todas las propiedades de ambos tipos a la vez. T | U (union, ya visto en Clase 1) exige una u otra forma, no las dos juntas.

Ejercicio 10 — Repositorio<T> (mini-proyecto integrador)

Combina todo lo de la clase: interfaz ConId { id: number }, clase genérica Repositorio<T extends ConId> con guardar, buscarPorId y listar, e interfaz Producto extends ConId como caso de uso real. Así se ve un genérico en un proyecto real (piensa en un mini ORM).

Ver solución
typescript
interface ConId {
  id: number;
}

class Repositorio<T extends ConId> {
  private items: T[] = [];

  guardar(item: T): void {
    this.items.push(item);
  }

  buscarPorId(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }

  listar(): T[] {
    return this.items;
  }
}

interface Producto extends ConId {
  nombre: string;
  precio: number;
}

const repo = new Repositorio<Producto>();
repo.guardar({ id: 1, nombre: "Teclado", precio: 150 });
repo.guardar({ id: 2, nombre: "Mouse", precio: 60 });

console.log(repo.buscarPorId(1));
console.log(repo.listar().length);

Terminal esperada:

zsh · repositorio.ts
$ tsc repositorio.ts
$ node repositorio.js
{ id: 1, nombre: 'Teclado', precio: 150 }
2

💡 Repositorio<T extends ConId> combina TODO lo de esta clase: clase genérica + genérico restringido + interfaz que extiende a otra + un caso de uso realista.

❓ Preguntas y respuestas (autoevaluación)

1. ¿Qué problema resuelve un genérico que any no resuelve?

Mantiene la relación de tipos entre entrada y salida. any acepta todo pero deja de verificar; un genérico acepta todo tipo y sigue verificando que sea consistente.

2. ¿Qué diferencia hay entre identity<string>("Hola") y identity("Hola")?

Ambas terminan igual: en la primera fijas T = string a mano; en la segunda TypeScript lo infiere solo a partir del argumento.

3. ¿Se pueden usar genéricos en clases, no solo en funciones?

Sí. class Caja<T> parametriza toda la clase: propiedades, constructor y métodos pueden usar ese mismo T.

4. Si haces new Caja<string>("Libros"), ¿esa instancia puede luego guardar un number?

No. Queda fija al tipo con el que se instanció (string en este caso); TypeScript marcaría error si intentas meter un number.

5. ¿Qué hace T extends { length: number } en un genérico (Ejercicio 6)?

Restringe qué tipos puede tomar T: solo acepta tipos que cumplan esa forma (que tengan al menos una propiedad length de tipo number, como strings o arrays), no cualquier tipo.

6. ¿Qué significa la T en un genérico? ¿Tiene que llamarse así?

Es solo una convención (de "Type"), no una palabra reservada. Podrías llamarlo X o Cualquiera y funcionaría igual — pero T (y U, K, V para varios parámetros) es el estándar que todo el mundo reconoce, así que conviene mantenerlo.

7. ¿Puede una interfaz ser genérica, igual que una función o una clase?

Sí. interface Contenedor<T> { ... } describe un contrato sin fijar el tipo; una clase la implementa (implements Contenedor<T>) y una función puede recibir cualquier cosa que cumpla esa interfaz, sin conocer la clase concreta.

8. ¿Por qué Cola<T> usa shift() y Pila<T> (vista en clases anteriores) usaría pop()?

Porque son estructuras distintas: Cola es FIFO (el primero que entra es el primero que sale — shift() saca del inicio del array) y Pila es LIFO (el último que entra es el primero que sale — pop() saca del final). La lógica genérica es idéntica; solo cambia de qué extremo del array se saca.

9. ¿Qué es un intersection type (T & U) y en qué se diferencia de un union type (T | U)?

T & U exige todas las propiedades de ambos tipos a la vez (como en fusionar<T, U> del Ejercicio 9). T | U exige una u otra forma, no las dos juntas. Son opuestos: unión = "o", intersección = "y".

10. ¿Qué combina el Repositorio<T> del Ejercicio 10 que lo hace un caso "integrador"?

Junta varias piezas de la clase en un solo ejemplo real: una clase genérica (Repositorio<T>), un genérico restringido (T extends ConId), y una interfaz que extiende a otra (Producto extends ConId) — el mismo patrón que usaría un mini ORM.

📎 Apuntes relacionados

➡️ Siguiente

Módulos (import/export a fondo) o Utility types (Partial, Pick, Omit, Record…).