As classes genéricas em TypeScript permitem escrever uma classe uma única vez e utilizá-la com segurança em muitos tipos diferentes. No desenvolvimento backend, a aplicação mais prática é o padrão de repositório: uma classe base genérica que gerencia operações CRUD para qualquer entidade, em vez de copiar os mesmos métodos em um UserRepository, ProductRepository e OrderRepository.


Sumário

  1. Entendendo Generics em TypeScript
  2. Benefícios do Uso de Classes Genéricas no Desenvolvimento Backend
  3. Criando Classes Genéricas
  4. Implementando Repositórios Genéricos
  5. Aplicando Restrições aos Generics
  6. Técnicas Avançadas de Classes Genéricas
  7. Exemplos do Mundo Real
  8. Boas Práticas
  9. Armadilhas Comuns e Como Evitá-las
  10. Conclusão

Entendendo Generics em TypeScript

Os generics permitem criar componentes reutilizáveis que funcionam com uma variedade de tipos, em vez de um único tipo. Isso garante segurança de tipos ao mesmo tempo que permite flexibilidade.

Exemplo básico de função genérica:

function identity<T>(arg: T): T {
  return arg;
}

Aqui, T é um parâmetro de tipo que atua como um marcador de posição para o tipo que será fornecido quando a função for chamada.


Benefícios do Uso de Classes Genéricas no Desenvolvimento Backend

O uso de classes genéricas no desenvolvimento backend oferece diversas vantagens:

  • Segurança de tipos: os generics fornecem verificação de tipos em tempo de compilação, reduzindo erros em tempo de execução.
  • Reutilização: escreva o código uma vez e use-o para múltiplos tipos sem duplicação.
  • Manutenibilidade: alterações na lógica compartilhada se propagam automaticamente para todos os chamadores.
  • Flexibilidade: lide com diferentes tipos e estruturas de dados sem conversão de tipos.

Criando Classes Genéricas

Uma classe genérica permite definir uma classe com um marcador de posição para o tipo de suas propriedades ou métodos.

Sintaxe de classe genérica:

class GenericClass<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

Exemplo 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 Repositórios Genéricos

Em aplicações backend, os repositórios encapsulam a lógica de acesso a dados. Um repositório genérico permite escrever essa lógica uma vez e aplicá-la a qualquer modelo.

Definindo uma interface de repositório 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 uma classe de repositório 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 o repositório 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 Restrições aos Generics

Às vezes é necessário garantir que um parâmetro de tipo tenha propriedades específicas. Use a palavra-chave extends para restringir os generics.

Usando a palavra-chave extends:

interface Identifiable {
  id: string;
}

class BaseEntityRepository<T extends Identifiable> {
  // Implementation...
}

Agora, T deve ser um tipo que possui uma propriedade id do tipo string.

Exemplo:

class Product implements Identifiable {
  id: string;
  price: number;
}

const productRepository = new BaseEntityRepository<Product>();

Técnicas Avançadas de Classes Genéricas

Usando Múltiplos Parâmetros de Tipo

É possível definir múltiplos parâmetros de tipo para cenários mais complexos.

Exemplo:

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 com Valor Padrão

É possível atribuir tipos padrão aos parâmetros de tipo.

Exemplo:

class Response<T = any> {
  data: T;
  status: number;

  constructor(data: T, status: number) {
    this.data = data;
    this.status = status;
  }
}

Exemplos do Mundo Real

Tratamento de Erros Genérico

Crie uma classe de resposta de erro genérica para padronizar o tratamento de erros.

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 no Express.js

Implemente funções de middleware reutilizáveis usando generics.

function genericMiddleware<T>(req: Request, res: Response, next: NextFunction) {
  // Middleware logic using generic type T
  next();
}

Boas Práticas

  • Use nomes descritivos para os parâmetros de tipo. TEntity ou TModel comunica a intenção melhor do que um simples T.
  • Não recorra a generics a menos que eles ofereçam valor claro. Uma função que recebe any e faz um log não precisa de um parâmetro de tipo.
  • Combine generics com interfaces e tipos de interseção para contratos mais rigorosos.
  • Documente assinaturas genéricas complexas. Um comentário bem posicionado poupa ao próximo desenvolvedor 20 minutos de engenharia reversa.

Armadilhas Comuns e Como Evitá-las

Apagamento de Tipos

Os parâmetros de tipo não estão disponíveis em tempo de execução. O TypeScript compila para JavaScript e apaga todas as informações de tipo, portanto não é possível usar T como um valor.

Abordagem incorreta:

class Factory<T> {
  create(): T {
    return new T(); // Error: 'T' cannot be used as a value
  }
}

Solução: passe o construtor da classe 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 Excessivo de Generics

Os generics adicionam carga cognitiva. Não os utilize quando uma assinatura mais simples é igualmente segura.

Exemplo de uso excessivo:

function logValue<T>(value: T): void {
  console.log(value);
}

// Not needed; simpler function suffices
function logValue(value: any): void {
  console.log(value);
}

Conclusão

As classes genéricas se mostram mais vantajosas nas camadas de repositório e serviço de uma aplicação backend. Escreva o padrão uma vez, parametrize pelo tipo de entidade e cada novo modelo recebe a mesma implementação bem testada. As garantias em tempo de compilação tornam as refatorações mais seguras também: renomeie uma propriedade em User e o compilador indica todos os lugares onde o repositório está passando a estrutura errada.

Mantenha as restrições rigorosas, os nomes dos parâmetros de tipo significativos e resista ao impulso de generificar código que terá apenas um único tipo concreto.


Referências