Generics em interfaces e classes
1. Introdução aos Generics em Estruturas de Dados
Generics permitem que interfaces e classes trabalhem com tipos de forma parametrizada, oferecendo reutilização de código sem sacrificar a segurança de tipos. Enquanto tipos concretos fixam uma estrutura específica, tipos parametrizados permitem que a mesma definição funcione com diferentes tipos.
Considere a diferença entre uma interface que só aceita números e uma genérica:
// Tipo concreto - funciona apenas com number
interface CaixaNumerica {
conteudo: number;
}
// Tipo parametrizado - funciona com qualquer tipo
interface Caixa<T> {
conteudo: T;
}
const caixaString: Caixa<string> = { conteudo: "olá" };
const caixaNumero: Caixa<number> = { conteudo: 42 };
O mesmo princípio se aplica a classes. Uma Pilha<T> pode armazenar qualquer tipo de elemento:
class Pilha<T> {
private elementos: T[] = [];
push(item: T): void {
this.elementos.push(item);
}
pop(): T | undefined {
return this.elementos.pop();
}
topo(): T | undefined {
return this.elementos[this.elementos.length - 1];
}
}
const pilhaNumeros = new Pilha<number>();
pilhaNumeros.push(10);
pilhaNumeros.push(20);
const pilhaStrings = new Pilha<string>();
pilhaStrings.push("TypeScript");
2. Interfaces Genéricas: Definição e Uso
Interfaces genéricas definem contratos que podem ser implementados para tipos específicos. Um exemplo clássico é um repositório de dados:
interface Repositorio<T> {
salvar(item: T): void;
buscar(id: string): T;
listar(): T[];
remover(id: string): void;
}
Implementação concreta para usuários:
interface Usuario {
id: string;
nome: string;
email: string;
}
class RepositorioUsuario implements Repositorio<Usuario> {
private dados: Usuario[] = [];
salvar(item: Usuario): void {
this.dados.push(item);
}
buscar(id: string): Usuario {
const usuario = this.dados.find(u => u.id === id);
if (!usuario) throw new Error("Usuário não encontrado");
return usuario;
}
listar(): Usuario[] {
return [...this.dados];
}
remover(id: string): void {
this.dados = this.dados.filter(u => u.id !== id);
}
}
Múltiplos parâmetros de tipo são úteis para estruturas como pares chave-valor:
interface Par<K, V> {
chave: K;
valor: V;
}
const par1: Par<string, number> = { chave: "idade", valor: 30 };
const par2: Par<number, boolean> = { chave: 1, valor: true };
3. Classes Genéricas: Propriedades, Métodos e Construtores
Classes genéricas podem usar o parâmetro de tipo em propriedades, métodos e construtores:
class Lista<T> {
private itens: T[] = [];
constructor(...itensIniciais: T[]) {
this.itens.push(...itensIniciais);
}
adicionar(item: T): void {
this.itens.push(item);
}
obter(indice: number): T {
if (indice < 0 || indice >= this.itens.length) {
throw new Error("Índice inválido");
}
return this.itens[indice];
}
filtrar(predicado: (item: T) => boolean): T[] {
return this.itens.filter(predicado);
}
get tamanho(): number {
return this.itens.length;
}
}
// Inferência de tipo na instanciação
const lista = new Lista("a", "b", "c"); // Tipo inferido: Lista<string>
const listaNumeros = new Lista<number>(1, 2, 3);
4. Constraints em Interfaces e Classes Genéricas
Constraints permitem restringir quais tipos podem ser usados com um generic, garantindo acesso a propriedades específicas:
interface Identificavel {
id: string;
}
class Repositorio<T extends Identificavel> {
private itens: T[] = [];
adicionar(item: T): void {
// Podemos acessar item.id com segurança
const existe = this.itens.find(i => i.id === item.id);
if (existe) {
throw new Error(`Item com id ${item.id} já existe`);
}
this.itens.push(item);
}
buscar(id: string): T | undefined {
return this.itens.find(item => item.id === id);
}
}
interface Produto extends Identificavel {
nome: string;
preco: number;
}
const repositorioProdutos = new Repositorio<Produto>();
repositorioProdutos.adicionar({ id: "p1", nome: "Teclado", preco: 150 });
Constraints também funcionam com tipos primitivos e interfaces complexas:
class Pilha<T extends { id: string; toString(): string }> {
private itens: T[] = [];
push(item: T): void {
console.log(`Adicionando: ${item.toString()}`);
this.itens.push(item);
}
pop(): T | undefined {
return this.itens.pop();
}
}
class Item {
constructor(public id: string, public valor: string) {}
toString(): string {
return `${this.id}: ${this.valor}`;
}
}
const pilhaItens = new Pilha<Item>();
pilhaItens.push(new Item("1", "Exemplo"));
5. Herança e Composição com Tipos Genéricos
Classes genéricas podem estender outras classes genéricas, e interfaces podem estender interfaces genéricas:
interface Colecao<T> {
adicionar(item: T): void;
remover(item: T): boolean;
}
interface ColecaoOrdenada<T> extends Colecao<T> {
ordenar(comparador: (a: T, b: T) => number): void;
}
class ListaOrdenada<T> implements ColecaoOrdenada<T> {
private itens: T[] = [];
adicionar(item: T): void {
this.itens.push(item);
}
remover(item: T): boolean {
const index = this.itens.indexOf(item);
if (index === -1) return false;
this.itens.splice(index, 1);
return true;
}
ordenar(comparador: (a: T, b: T) => number): void {
this.itens.sort(comparador);
}
}
Composição com tipos genéricos aninhados:
class Container<T> {
constructor(public valor: T) {}
}
class CaixaCompartimentada<T, U> {
constructor(
public compartimento1: Container<T>,
public compartimento2: Container<U>
) {}
}
const caixa = new CaixaCompartimentada(
new Container("texto"),
new Container(42)
);
6. Métodos Estáticos e Genéricos em Classes
Métodos estáticos não podem acessar o parâmetro de tipo da classe, mas podem definir seus próprios parâmetros genéricos:
class Lista<T> {
private itens: T[] = [];
constructor(...itens: T[]) {
this.itens.push(...itens);
}
// Método estático com seu próprio parâmetro genérico
static criar<T>(...args: T[]): Lista<T> {
return new Lista<T>(...args);
}
// Método de instância normal
adicionar(item: T): void {
this.itens.push(item);
}
}
// Uso do método estático genérico
const lista1 = Lista.criar(1, 2, 3); // Lista<number>
const lista2 = Lista.criar("a", "b"); // Lista<string>
Outro exemplo com factory estático:
class Par<K, V> {
constructor(
public chave: K,
public valor: V
) {}
static deArray<T>(items: T[]): Par<number, T>[] {
return items.map((item, index) => new Par(index, item));
}
}
const pares = Par.deArray(["azul", "verde", "vermelho"]);
// Tipo: Par<number, string>[]
7. Padrões e Boas Práticas com Generics
Convenções de nomenclatura comuns:
| Nome | Uso típico |
|---|---|
T |
Tipo genérico principal |
K |
Chave (Key) |
V |
Valor (Value) |
U |
Segundo tipo genérico |
TItem |
Tipo específico de item |
Exemplos do mundo real:
// Observable Pattern
class Observable<T> {
private observadores: Array<(dado: T) => void> = [];
inscrever(callback: (dado: T) => void): void {
this.observadores.push(callback);
}
notificar(dado: T): void {
this.observadores.forEach(cb => cb(dado));
}
}
// API Response pattern
interface ApiResponse<T> {
dados: T;
mensagem: string;
sucesso: boolean;
codigo: number;
}
async function buscarUsuario(id: string): Promise<ApiResponse<{ nome: string; email: string }>> {
const resposta = await fetch(`/api/usuarios/${id}`);
return resposta.json();
}
Quando evitar generics:
- Se a interface ou classe só funcionará com um tipo específico
- Se o código ficar mais complexo sem benefício real
- Se a generalização prejudicar a legibilidade
// Evite: supergeneralização desnecessária
interface Container<T, U, V> {
item1: T;
item2: U;
item3: V;
}
// Prefira: específico quando só há um caso de uso
interface Endereco {
rua: string;
numero: number;
cidade: string;
}
Generics em interfaces e classes são ferramentas poderosas para criar código reutilizável e type-safe. Use-os quando precisar de flexibilidade sem abrir mão do sistema de tipos do TypeScript.
Referências
- TypeScript Handbook: Generics — Documentação oficial sobre generics, incluindo interfaces e classes genéricas
- TypeScript: Classes Genéricas — Seção específica sobre classes genéricas na documentação oficial
- TypeScript Deep Dive: Generics — Guia aprofundado sobre generics com exemplos práticos
- Total TypeScript: Generics Guide — Tutorial completo sobre generics em TypeScript com foco em padrões avançados
- TypeScript Generics in Action — Playground oficial com exemplos interativos de classes e interfaces genéricas