Introducción: qué son los Generics y por qué son importantes
TypeScript es un lenguaje fuertemente tipado que añade un potente sistema de tipos a JavaScript. Sin embargo, el tipado estricto a veces dificulta escribir código flexible y reutilizable. Aquí es donde entran en juego los Generics (genéricos, tipos genéricos).
Los Generics son una herramienta que permite crear componentes (funciones, clases, interfaces) que trabajan con diferentes tipos de datos sin perder el control de tipos. Imagine que escribe una función que debe funcionar tanto con números, como con cadenas y arrays. Sin genéricos, tendría que usar any (lo que anula todos los beneficios del tipado) o crear funciones separadas para cada tipo de dato. Los Generics resuelven este problema de manera elegante y segura.
En este artículo, analizaremos para qué sirven los genéricos en TypeScript y, lo más importante, cómo usarlos en la práctica, desde funciones simples hasta clases complejas y restricciones de tipos.
Parte principal
1. Generics en funciones: de la teoría a la práctica
El ejemplo más simple y común de uso de genéricos son las funciones que reciben y devuelven datos del mismo tipo. Consideremos una tarea clásica: una función que devuelve el primer elemento de un array.
Enfoque incorrecto (sin Generics):
// Ejemplo con any — perdemos información del tipofunction getFirst(arr: any[]): any { return arr[0];}
const num = getFirst([1, 2, 3]); // tipo num — any, no sabemos que es un númeroEnfoque correcto (con Generics):
// Usamos el tipo genérico Tfunction getFirst<T>(arr: T[]): T { return arr[0];}
const num = getFirst([1, 2, 3]); // tipo num — numberconst str = getFirst(['a', 'b', 'c']); // tipo str — stringconst bool = getFirst([true, false]); // tipo bool — boolean¿Cómo funciona esto? Declaramos un parámetro de tipo <T> (el nombre puede ser cualquiera, pero por convención se usan T, U, V). TypeScript infiere automáticamente el tipo concreto basándose en el argumento pasado. Esto se llama inferencia de tipos.
También puede especificar explícitamente el tipo al llamar a la función:
const num = getFirst<number>([1, 2, 3]); // especificación explícita del tipoOtro ejemplo útil es una función que devuelve un elemento aleatorio de un array:
function getRandom<T>(items: T[]): T { const randomIndex = Math.floor(Math.random() * items.length); return items[randomIndex];}
const randomNumber = getRandom([10, 20, 30]); // tipo — numberconst randomName = getRandom(['Ana', 'Iván', 'María']); // tipo — string2. Generics en interfaces y tipos
Los genéricos también funcionan muy bien con interfaces. Esto permite crear estructuras de datos flexibles que pueden almacenar valores de diferentes tipos.
Imaginemos que queremos describir una respuesta de API que puede contener datos de cualquier tipo:
interface ApiResponse<T> { status: number; message: string; data: T;}
// Uso para diferentes tipos de datosconst userResponse: ApiResponse<{ id: number; name: string }> = { status: 200, message: 'Éxito', data: { id: 1, name: 'Alejandro' }};
const listResponse: ApiResponse<string[]> = { status: 200, message: 'Lista obtenida', data: ['manzana', 'plátano', 'naranja']};También se pueden usar genéricos con alias de tipos (type):
type Result<T> = { success: boolean; value: T; error?: string;};
const successResult: Result<number> = { success: true, value: 42 };const errorResult: Result<string> = { success: false, error: 'Error de carga', value: '' };Tenga en cuenta: podemos co