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

  1. Entendiendo los Generics en TypeScript
  2. Beneficios del Uso de Clases Genéricas en el Desarrollo Backend
  3. Creando Clases Genéricas
  4. Implementando Repositorios Genéricos
  5. Aplicando Restricciones a los Generics
  6. Técnicas Avanzadas de Clases Genéricas
  7. Ejemplos del Mundo Real
  8. Buenas Prácticas
  9. Errores Comunes y Cómo Evitarlos
  10. 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. TEntity o TModel comunica la intención mejor que un simple T.
  • No recurras a los generics a menos que aporten un valor claro. Una función que recibe any y 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