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
- Entendendo Generics em TypeScript
- Benefícios do Uso de Classes Genéricas no Desenvolvimento Backend
- Criando Classes Genéricas
- Implementando Repositórios Genéricos
- Aplicando Restrições aos Generics
- Técnicas Avançadas de Classes Genéricas
- Exemplos do Mundo Real
- Boas Práticas
- Armadilhas Comuns e Como Evitá-las
- 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.
TEntityouTModelcomunica a intenção melhor do que um simplesT. - Não recorra a generics a menos que eles ofereçam valor claro. Uma função que recebe
anye 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
- TypeScript Handbook - Generics
- TypeScript Generics Tutorial
- Implementing the Repository Pattern in TypeScript