Entiende los genéricos en TypeScript con un ejemplo real

Si has trabajado con TypeScript, es muy probable que hayas escuchado sobre los genéricos, los cuales se usan en tipos como arrays o promesas. Pero, ¿alguna vez entendiste qué son, por qué existen y cuándo utilizarlos?
Permíteme darte un breve resumen. Primer punto: ten en cuenta que siempre debes usar tipos para tus objetos y evitar el tipo any para hacer tu código flexible. Con estos dos puntos en mente, empecemos con los genéricos.
Los genéricos básicamente te permiten crear código reutilizable que no está atado a un solo tipo.
Te voy a mostrar un ejemplo con código: necesito crear una caja para guardar mis fotos, además de un manejador de ayuda para agregar, obtener y eliminar.
Mi solución:
- Crear la clase Photo con la información de la entidad foto.
- Crear la clase Manager. Contiene un arreglo de fotos y acciones para agregar, eliminar y obtener.
- La clase Box con la propiedad photos, que funciona usando el manejador.
class Photo {
readonly id = 1;
constructor(public name: string){ }
}
class Manager {
items: Photo[] = []
add(item: Photo) {
this.items.push(item);
}
remove(item: Photo){
this.items = this.items.filter( i => i.id === item.id);
}
get(id: number): Photo | undefined {
return this.items.find(i => i.id === id)
}
}
class Box {
photos: Manager = new Manager();
}
let box = new Box();
box.photos.add(new Photo("hello.jpg"));
console.log(box.photos.items);Esto funciona, pero mañana podríamos querer permitir agregar videos. La primera solución sería duplicar la clase manager para la nueva clase video. La segunda solución sería usar el tipo any dentro de la clase manager, pero eso es peligroso porque el manejador guardaría cualquier tipo de objeto sin ninguna restricción, y eso no es lo que queremos.
Quizás usar una interface sea una solución, ya que el manejador se vincula a la interfaz y, si el video cumple con ella, puedo guardar cualquier clase. Sin embargo, la clase necesita implementar la interfaz, así que vamos a probarlo.
interface Item {
readonly id: number;
}
class Photo implements Item{
readonly id = 1;
constructor(public name: string){ }
}
class Video implements Item {
readonly id = 1;
quality: string = "4K"
}
class Manager {
items: Item[] = []
add(item: Item) {
this.items.push(item);
}
remove(item: Item){
this.items = this.items.filter( i => i.id === item.id);
}
get(id: number): Item | undefined {
return this.items.find(i => i.id === id)
}
}
class Box {
photos: Manager = new Manager();
videos: Manager = new Manager();
}
let box = new Box();
box.photos.add(new Photo("hello.jpg"));
box.videos.add(new Video());
box.videos.add(new Photo("fuck.mp4"));
console.log(box.photos.items);
console.log(box.videos.items);Funciona y obliga a que cada objeto sea un Item, pero el manejador de fotos puede guardar videos, y este no era mi plan. Aquí es donde los genéricos vienen a ayudarnos.
Al usar genéricos definimos el tipo para el manejador usando la palabra clave <T>, forzamos el tipo especificado al declarar el manejador, y las funciones y clases quedan asociadas a ese tipo en tiempo de ejecución.
interface Item {
readonly id: number;
}
class Photo implements Item{
readonly id = 1;
constructor(public name: string){
}
}
class Video implements Item {
readonly id = 1;
quality: string = "4K"
}
class Manager<T extends Item> {
items: T[] = []
add(item: T) {
this.items.push(item);
}
remove(item: T){
this.items = this.items.filter( i => i.id === item.id);
}
get(id: number): T | undefined {
return this.items.find(i => i.id === id)
}
}
class Box {
photos: Manager<Photo> = new Manager<Photo>();
videos: Manager<Video> = new Manager<Video>();
}
let box = new Box();
box.photos.add(new Photo("hello.jpg"));
box.videos.add(new Video());
box.videos.add(new Photo("fuck.mp4"));
// El compilador y el IDE muestran un error indicando que la foto no puede guardarse en la lista de videos
console.log(box.photos.items)
console.log(box.videos.items)Si intentamos agregar una foto al manejador de videos, el IDE y el compilador mostrarán un error porque estás intentando agregar una foto dentro del arreglo de videos.
Como puedes ver, los genéricos son una excelente forma de escribir código flexible y reutilizable manteniendo las reglas de tipado.
¿Cuándo podemos usar genéricos? Cuando detectamos comportamientos que se repiten con diferentes tipos y que se pueden utilizar en funciones o clases.
Espero que esto te sirva de ayuda con los genéricos en TypeScript. Si te gustó este artículo, ¡no olvides compartirlo!
Artículos Relacionados
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.