Las clases genéricas en TypeScript permiten escribir una clase una sola vez y utilizarla de forma segura con muchos tipos diferentes. En el desarrollo backend, la aplicación más práctica es el patrón repositorio: una clase base genérica que gestiona operaciones CRUD para cualquier entidad, en lugar de copiar los mismos métodos en un UserRepository, ProductRepository y OrderRepository.
Tabla de Contenidos
- Entendiendo los Generics en TypeScript
- Beneficios del Uso de Clases Genéricas en el Desarrollo Backend
- Creando Clases Genéricas
- Implementando Repositorios Genéricos
- Aplicando Restricciones a los Generics
- Técnicas Avanzadas de Clases Genéricas
- Ejemplos del Mundo Real
- Buenas Prácticas
- Errores Comunes y Cómo Evitarlos
- Conclusión
Entendiendo los Generics en TypeScript
Los generics permiten crear componentes reutilizables que funcionan con una variedad de tipos en lugar de uno solo. Esto garantiza la seguridad de tipos a la vez que habilita la flexibilidad.
Ejemplo básico de función genérica:
function identity<T>(arg: T): T {
return arg;
}
Aquí, T es un parámetro de tipo que actúa como marcador de posición para el tipo que se proporcionará cuando se llame a la función.
Beneficios del Uso de Clases Genéricas en el Desarrollo Backend
El uso de clases genéricas en el desarrollo backend ofrece varias ventajas:
- Seguridad de tipos: los generics proporcionan verificación de tipos en tiempo de compilación, reduciendo errores en tiempo de ejecución.
- Reutilización: escribe el código una vez y úsalo para múltiples tipos sin duplicación.
- Mantenibilidad: los cambios en la lógica compartida se propagan automáticamente a todos los llamadores.
- Flexibilidad: maneja diferentes tipos y estructuras de datos sin conversión de tipos.
Creando Clases Genéricas
Una clase genérica permite definir una clase con un marcador de posición para el tipo de sus propiedades o métodos.
Sintaxis de clase genérica:
class GenericClass<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
Ejemplo de uso:
const numberInstance = new GenericClass<number>(42);
console.log(numberInstance.getValue()); // Output: 42
const stringInstance = new GenericClass<string>("Hello, World!");
console.log(stringInstance.getValue()); // Output: Hello, World!
Implementando Repositorios Genéricos
En las aplicaciones backend, los repositorios encapsulan la lógica de acceso a datos. Un repositorio genérico permite escribir esa lógica una sola vez y aplicarla a cualquier modelo.
Definiendo una interfaz de repositorio genérico:
interface IRepository<T> {
getById(id: string): Promise<T | null>;
getAll(): Promise<T[]>;
add(entity: T): Promise<void>;
update(entity: T): Promise<void>;
delete(id: string): Promise<void>;
}
Implementando una clase de repositorio genérico:
class Repository<T> implements IRepository<T> {
private collection: string;
constructor(collection: string) {
this.collection = collection;
}
async getById(id: string): Promise<T | null> {
// Implement data retrieval logic here
return null;
}
async getAll(): Promise<T[]> {
// Implement data retrieval logic here
return [];
}
async add(entity: T): Promise<void> {
// Implement data insertion logic here
}
async update(entity: T): Promise<void> {
// Implement data update logic here
}
async delete(id: string): Promise<void> {
// Implement data deletion logic here
}
}
Usando el repositorio genérico:
interface User {
id: string;
name: string;
email: string;
}
const userRepository = new Repository<User>("users");
userRepository.add({ id: "1", name: "Alice", email: "alice@example.com" });
Aplicando Restricciones a los Generics
A veces es necesario asegurarse de que un parámetro de tipo tenga propiedades específicas. Usa la palabra clave extends para restringir los generics.
Usando la palabra clave extends:
interface Identifiable {
id: string;
}
class BaseEntityRepository<T extends Identifiable> {
// Implementation...
}
Ahora, T debe ser un tipo que tenga una propiedad id de tipo string.
Ejemplo:
class Product implements Identifiable {
id: string;
price: number;
}
const productRepository = new BaseEntityRepository<Product>();
Técnicas Avanzadas de Clases Genéricas
Uso de Múltiples Parámetros de Tipo
Es posible definir múltiples parámetros de tipo para escenarios más complejos.
Ejemplo:
class MapEntry<K, V> {
key: K;
value: V;
constructor(key: K, value: V) {
this.key = key;
this.value = value;
}
}
const entry = new MapEntry<number, string>(1, "One");
Parámetros de Tipo con Valor por Defecto
Es posible asignar tipos predeterminados a los parámetros de tipo.
Ejemplo:
class Response<T = any> {
data: T;
status: number;
constructor(data: T, status: number) {
this.data = data;
this.status = status;
}
}
Ejemplos del Mundo Real
Manejo de Errores Genérico
Crea una clase de respuesta de error genérica para estandarizar el manejo de errores.
class ErrorResponse<T> {
error: T;
message: string;
constructor(error: T, message: string) {
this.error = error;
this.message = message;
}
}
interface ValidationError {
field: string;
issue: string;
}
const validationErrorResponse = new ErrorResponse<ValidationError>(
{ field: "email", issue: "Invalid format" },
"Validation Error"
);
Middleware Genérico en Express.js
Implementa funciones de middleware reutilizables usando generics.
function genericMiddleware<T>(req: Request, res: Response, next: NextFunction) {
// Middleware logic using generic type T
next();
}
Buenas Prácticas
- Usa nombres descriptivos para los parámetros de tipo.
TEntityoTModelcomunica la intención mejor que un simpleT. - No recurras a los generics a menos que aporten un valor claro. Una función que recibe
anyy hace un log no necesita un parámetro de tipo. - Combina generics con interfaces y tipos de intersección para contratos más estrictos.
- Documenta las firmas genéricas complejas. Un comentario bien ubicado le ahorra al siguiente desarrollador 20 minutos de ingeniería inversa.
Errores Comunes y Cómo Evitarlos
Borrado de Tipos
Los parámetros de tipo no están disponibles en tiempo de ejecución. TypeScript compila a JavaScript y elimina toda la información de tipos, por lo que no es posible usar T como un valor.
Enfoque incorrecto:
class Factory<T> {
create(): T {
return new T(); // Error: 'T' cannot be used as a value
}
}
Solución: pasa el constructor de la clase como parámetro.
class Factory<T> {
type: new () => T;
constructor(type: new () => T) {
this.type = type;
}
create(): T {
return new this.type();
}
}
class User {
name: string;
}
const userFactory = new Factory(User);
const user = userFactory.create();
Uso Excesivo de Generics
Los generics añaden carga cognitiva. No los uses cuando una firma más simple es igual de segura.
Ejemplo de uso excesivo:
function logValue<T>(value: T): void {
console.log(value);
}
// Not needed; simpler function suffices
function logValue(value: any): void {
console.log(value);
}
Conclusión
Las clases genéricas son más rentables en las capas de repositorio y servicio de una aplicación backend. Escribe el patrón una vez, parametriza por tipo de entidad y cada nuevo modelo obtiene la misma implementación probada en batalla. Las garantías en tiempo de compilación hacen que las refactorizaciones también sean más seguras: renombra una propiedad en User y el compilador te indica en todos los lugares donde el repositorio está pasando la estructura incorrecta.
Mantén las restricciones ajustadas, los nombres de los parámetros de tipo con significado, y resiste el impulso de genericizar código que solo tendrá un único tipo concreto.
Referencias
- TypeScript Handbook - Generics
- TypeScript Generics Tutorial
- Implementing the Repository Pattern in TypeScript