Apariencia
📙 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érmino | Qué es | Ejemplo de esta clase |
|---|---|---|
Parámetro T | El "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érica | Una 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érica | Una 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 tipada | El 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
Tes 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, eseTdeja de ser un hueco y se vuelve un tipo concreto y fijo.

📝 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 usarany— pero ahí TypeScript deja de vigilar por completo:typescriptfunction identityAny(arg: any): any { return arg; } let a = identityAny("hola"); a.toFixed(2); // 💥 esto truena al ejecutar, pero TS no avisó nadaCon un genérico (
<T>) sigues aceptando cualquier tipo, pero TypeScript se acuerda cuál entró y lo sigue verificando:typescriptfunction 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"):
| Tipo | Acepta 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 contypeofantes 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:
unknownvs genérico → ambos son seguros, pero resuelven cosas distintas.unknownes 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: Tdice "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.anyapaga 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:

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);| Pieza | Mnemotécnico | En 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 string | espera un string | devuelve un string |
identidade<number>(100) | quiere un number | espera un number | devuelve un number |
identidade<boolean>(true) | quiere un boolean | espera un boolean | devuelve 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| Forma | Cómo se escribe | Cuándo usarla |
|---|---|---|
| Explícita | identity<string>("Hola") | Cuando el argumento no deja claro el tipo, o quieres dejarlo documentado |
| Inferida | identity(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 porT.private contenido: T— la propiedad usa ese mismo tipo.- El constructor y
obtenerContenido()reutilizanTpara 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 astring. No puedes luego meterle unnumber— 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ósitoTypeScript 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
TS2322que verías con cualquier variablestringnormal — 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)); // 2026Los tres conceptos trabajando juntos:
interface Contenedor<T>— el contrato: "cualquier cosa que implemente esto tienecontenidoyobtenerContenido(), del mismo tipoT".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 interfazContenedor<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 conCajaGenerica.
🎯 6. ¿Cuándo uso función, clase o interfaz genérica?
No son intercambiables — cada una responde a una necesidad distinta:
| Necesito... | Uso | Ejemplo de esta clase |
|---|---|---|
| Una utilidad reutilizable, sin estado | Función genérica | identity<T>, primerElemento<T> |
| Modelar una entidad con estado (guarda datos) | Clase genérica | Caja<T>, Pila<T> |
| Describir un contrato que varias clases pueden cumplir | Interfaz genérica | Contenedor<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.jsSalida real verificada:
$ tsc
$ node main.js
Output 1: Hola
Output 2: 42
Contenido de la caja de string: LibrosTodo verificado y corriendo. ✅
📁 Tip de repo: al crear la carpeta de un ejercicio nuevo, evita tildes en el nombre (
Genericos, noGenéricos) — algunas herramientas de Git/GitHub y de compilación siguen dando problemas con acentos en rutas.02-Ejercicios/Genericosya 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:
$ tsc identidad.ts
$ node identidad.js
TypeScript
2026
trueEjercicio 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:
$ tsc ultimo-elemento.ts
$ node ultimo-elemento.js
30
cEjercicio 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:
$ 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:
$ tsc cola.ts
$ node cola.js
primero
segundo
false💡
Cola(FIFO,shift()saca el primero) vsPila(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:
$ tsc par.ts
$ node par.js
30 StypEjercicio 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:
$ 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 paraT.
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:
$ 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:
$ tsc ordenar.ts
$ node ordenar.js
[ 1, 2, 5, 8 ]💡
ordenarno sabe cómo se comparan los valores — eso se lo delega a cualquierComparador<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:
$ 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:
$ 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.
anyacepta 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 = stringa 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 mismoT.
4. Si haces new Caja<string>("Libros"), ¿esa instancia puede luego guardar un number?
No. Queda fija al tipo con el que se instanció (
stringen este caso); TypeScript marcaría error si intentas meter unnumber.
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 propiedadlengthde tiponumber, 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
XoCualquieray funcionaría igual — peroT(yU,K,Vpara 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:
Colaes FIFO (el primero que entra es el primero que sale —shift()saca del inicio del array) yPilaes 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 & Uexige todas las propiedades de ambos tipos a la vez (como enfusionar<T, U>del Ejercicio 9).T | Uexige 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…).