Genéricos de TypeScript para el código diario: Guía práctica
Estás escribiendo una función que funciona con cualquier tipo, pero no quieres perder la seguridad de tipos. Podrías usar any, pero eso anula el propósito de TypeScript. Los genéricos te permiten escribir código que funciona con múltiples tipos mientras conserva la información de tipo. Esta guía te muestra cómo usar genéricos en el código diario sin complicar las cosas.
¿Qué son los genéricos?
Los genéricos son una forma de crear componentes reutilizables que funcionan con una variedad de tipos en lugar de uno solo. Te permiten capturar el tipo de la entrada y usarlo en la salida, garantizando la seguridad de tipos sin sacrificar la flexibilidad.
Piensa en los genéricos como parámetros de tipo. Así como una función toma parámetros de valor, una función genérica toma parámetros de tipo. Por ejemplo, la función identidad:
function identity<T>(arg: T): T {
return arg;
}
Aquí, T es un parámetro de tipo. Cuando llamas a identity<string>("hello"), TypeScript infiere que T es string, y el tipo de retorno es string. Si pasas un número, devuelve un número. Esto es más preciso que usar any.
¿Por qué no usar simplemente any?
Usar any desactiva la comprobación de tipos. Pierdes el autocompletado, el soporte de refactorización y el compilador no puede detectar errores. Los genéricos mantienen la información de tipo fluyendo a través de tu código, por lo que obtienes tanto flexibilidad como seguridad.
Funciones genéricas en la práctica
Veamos un escenario común: una función que devuelve el primer elemento de un array.
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
Sin genéricos, podrías escribir function firstElement(arr: any[]): any, lo que pierde el tipo. Con genéricos, si pasas un string[], el tipo de retorno es string | undefined.
También puedes usar múltiples parámetros de tipo. Por ejemplo, una función que mapea un array a un nuevo array usando un callback:
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
Así es esencialmente como se tipa Array.prototype.map en la biblioteca estándar de TypeScript.
Interfaces genéricas y alias de tipo
Los genéricos no se limitan a funciones. Puedes crear interfaces genéricas y alias de tipo para definir estructuras de datos reutilizables.
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
Esto es útil para respuestas de API, gestión de estado o cualquier tipo de contenedor.
También puedes proporcionar tipos predeterminados:
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
Ahora ApiResponse sin un argumento de tipo usa unknown por defecto.
Restricciones genéricas
A veces quieres restringir los tipos que se pueden usar con un genérico. Puedes usar la palabra clave extends para agregar una restricción.
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
Ahora T debe tener una propiedad length. Puedes llamar a getLength("hello") o getLength([1,2,3]), pero no a getLength(42).
Las restricciones son potentes cuando se trabaja con objetos que comparten una forma común.
Uso de genéricos con clases
Las clases también pueden ser genéricas. Un ejemplo común es una colección genérica:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const popped = stack.pop(); // number | undefined
Esto asegura que solo se puedan apilar números en una pila de números.
Ejemplo del mundo real: un emisor de eventos con seguridad de tipos
Construyamos un emisor de eventos simple que use genéricos para mapear nombres de eventos a tipos de payload.
type EventMap = Record<string, any>;
class EventEmitter<T extends EventMap> {
private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
this.listeners[event]?.forEach(listener => listener(payload));
}
}
// Uso
interface MyEvents {
login: { userId: string };
logout: { userId: string };
}
const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (payload) => {
console.log(payload.userId); // string
});
emitter.emit('login', { userId: '123' }); // OK
// emitter.emit('login', { userId: 123 }); // Error
Este patrón te brinda total seguridad de tipos para nombres de eventos y payloads, reduciendo errores en tiempo de ejecución.
Errores comunes y mejores prácticas
- No abuses de los genéricos. Si una función solo funciona con un tipo específico, no la hagas genérica.
- Usa nombres descriptivos para los parámetros de tipo.
Testá bien para casos simples, peroTItemoTResponsepueden ser más claros. - Prefiere restricciones para evitar errores en tiempo de ejecución. Si necesitas una propiedad, agrega una restricción.
- Aprovecha la inferencia de tipos. A menudo no necesitas pasar argumentos de tipo explícitamente; TypeScript puede inferirlos.
Comparación: genéricos vs. any vs. tipos unión
| Enfoque | Seguridad de tipos | Flexibilidad | Caso de uso |
|---|---|---|---|
| Genéricos | Alta | Alta | Componentes reutilizables con tipos preservados |
any |
Ninguna | Alta | Prototipado rápido, migración de JS |
| Tipos unión | Media | Media | Conjunto limitado de tipos conocidos |
Los genéricos logran un equilibrio entre flexibilidad y seguridad, lo que los hace ideales para bibliotecas y utilidades compartidas.
Preguntas frecuentes
¿Cuándo debo usar genéricos en lugar de tipos unión?
Usa genéricos cuando la relación de tipo entre entradas y salidas importa. Los tipos unión son para cuando un valor puede ser uno de un conjunto fijo de tipos, pero el tipo específico no necesita preservarse a través de las operaciones.
¿Puedo usar genéricos con funciones flecha?
Sí, pero la sintaxis puede ser complicada en archivos .tsx. Puedes escribir const identity = <T,>(arg: T): T => arg; con una coma final para evitar la ambigüedad con JSX.
¿Cómo restringo un genérico para que solo acepte objetos?
Usa T extends object o T extends Record<string, unknown> para asegurar que T sea un tipo de objeto.
Los genéricos son una piedra angular del sistema de tipos de TypeScript. Al usarlos en tu código diario, puedes escribir funciones, interfaces y clases que sean tanto flexibles como seguras. Comienza con funciones genéricas simples, luego incorpora gradualmente restricciones y patrones más complejos según sea necesario. Para más herramientas de desarrollo, consulta nuestro JSON Formatter para validar y formatear tus datos JSON rápidamente.