Genéricos de TypeScript para el código diario
Estás escribiendo una función que devuelve el primer elemento de un array. Quieres que funcione para números, cadenas y objetos, pero también quieres que el tipo de retorno coincida con el tipo de entrada. Sin genéricos, recurrirías a any y perderías toda la seguridad de tipos. Ahí es donde entran los genéricos de TypeScript.
Los genéricos te permiten escribir código que funciona con múltiples tipos mientras preserva la información del tipo específico. En esta guía, exploraremos patrones prácticos para usar genéricos en el código TypeScript del día a día.
¿Qué son los genéricos?
Los genéricos son una forma de parametrizar tipos. Igual que las funciones toman parámetros, los genéricos toman parámetros de tipo. Te permiten crear componentes reutilizables que funcionan con una variedad de tipos en lugar de uno solo.
Considera esta función identidad:
function identity<T>(arg: T): T {
return arg;
}
Aquí, T es una variable de tipo. Cuando llamas a identity(42), TypeScript infiere T como number. Cuando llamas a identity("hello"), T se convierte en string. El tipo de retorno siempre coincide con el tipo de entrada.
Funciones genéricas en la práctica
Veamos un escenario común: una función que envuelve un valor en un objeto.
function wrapInObject<T>(value: T): { value: T } {
return { value };
}
Sin genéricos, tendrías que usar any o unknown, y quienes llamen perderían la información de tipo. Con genéricos, el tipo de retorno es precisamente { value: T }.
Otro ejemplo cotidiano es una función que devuelve el último elemento de un array:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
TypeScript infiere T del argumento array, así que last([1, 2, 3]) devuelve number | undefined.
Restricciones genéricas
A veces quieres restringir los tipos que se pueden usar con tu genérico. Por ejemplo, podrías necesitar acceder a una propiedad que solo existe en ciertos tipos. Puedes usar la palabra clave extends para añadir una restricción.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
Ahora T debe tener una propiedad length de tipo number. Esto funciona para cadenas, arrays y cualquier objeto con una propiedad length.
También puedes usar restricciones con interfaces:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
Esto asegura que los elementos pasados a findById tengan una propiedad id.
Interfaces genéricas y alias de tipo
Los genéricos no se limitan a las funciones. Puedes definir interfaces genéricas y alias de tipo para crear estructuras de datos flexibles.
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return { data, status: response.status };
}
Ahora puedes tipar las respuestas de API con precisión:
interface User {
id: number;
name: string;
}
const userResponse = await fetchData<User>('/api/user');
// userResponse.data es User
Clases genéricas
Las clases también pueden ser genéricas. Un ejemplo común es una pila simple:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined
Genéricos en componentes React
Si estás construyendo componentes React, los genéricos pueden hacer que tus componentes sean más reutilizables. Por ejemplo, un componente de lista que renderiza elementos de cualquier tipo:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}
Ahora puedes usar List con cualquier tipo y obtener verificación de tipos completa en el callback renderItem.
Errores comunes y buenas prácticas
- No abuses de los genéricos. Si una función solo funciona con un tipo específico, simplemente usa ese tipo. Los genéricos añaden complejidad.
- Usa nombres descriptivos para los parámetros de tipo. Aunque
Tes común, para múltiples parámetros usa nombres comoTInput,TOutput. - Prefiere restricciones antes que
any. Las restricciones te dan seguridad de tipos sin dejar de ser flexible. - Recuerda que los genéricos se borran en tiempo de ejecución. Solo existen en tiempo de compilación.
Comparación: genéricos vs. any vs. tipos unión
| Enfoque | Seguridad de tipos | Flexibilidad | Caso de uso |
|---|---|---|---|
any |
Ninguna | Alta | Migrar JS, prototipos rápidos |
| Tipos unión | Buena | Limitada a tipos conocidos | Cuando conoces todos los tipos posibles |
| Genéricos | Fuerte | Alta | Componentes reutilizables con tipos preservados |
Preguntas frecuentes
¿Cuándo debería usar genéricos en lugar de any?
Usa genéricos siempre que quieras preservar la información de tipo a través de una función o componente. any desactiva la verificación de tipos, mientras que los genéricos la mantienen. Si te encuentras recurriendo a any para que algo funcione, considera si un genérico podría resolverlo de forma más segura.
¿Puedo usar múltiples parámetros de tipo?
Sí. Puedes definir funciones con múltiples parámetros de tipo, como function pair<K, V>(key: K, value: V): [K, V]. Usa nombres descriptivos cuando haya más de uno.
¿Cómo funcionan los genéricos con funciones flecha en archivos .tsx?
En archivos .tsx, <T> puede ser ambiguo con JSX. Usa una coma final: const identity = <T,>(arg: T): T => arg; o usa extends unknown: <T extends unknown>.
Los genéricos son una característica poderosa que puede hacer que tu código TypeScript sea más reutilizable y seguro en cuanto a tipos. Empieza con funciones genéricas simples, luego aplícalas gradualmente a interfaces y clases. Si necesitas formatear o validar datos JSON con seguridad de tipos, echa un vistazo a nuestra herramienta JSON Formatter para inspeccionar y formatear tu JSON rápidamente.