Generics do TypeScript para o dia a dia: guia prático
Você está escrevendo uma função que funciona com qualquer tipo, mas não quer perder a segurança de tipos. Você poderia usar any, mas isso anula o propósito do TypeScript. Generics permitem escrever código que funciona com vários tipos preservando as informações de tipo. Este guia mostra como usar generics no código do dia a dia sem complicar as coisas.
O que são Generics?
Generics são uma forma de criar componentes reutilizáveis que funcionam com uma variedade de tipos em vez de um único. Eles permitem capturar o tipo da entrada e usá-lo na saída, garantindo segurança de tipos sem sacrificar a flexibilidade.
Pense em generics como parâmetros de tipo. Assim como uma função recebe parâmetros de valor, uma função genérica recebe parâmetros de tipo. Por exemplo, a função identidade:
function identity<T>(arg: T): T {
return arg;
}
Aqui, T é um parâmetro de tipo. Quando você chama identity<string>("hello"), o TypeScript infere que T é string, e o tipo de retorno é string. Se você passar um número, ele retorna um número. Isso é mais preciso do que usar any.
Por que não usar apenas any?
Usar any desativa a verificação de tipos. Você perde o autocompletar, o suporte à refatoração e o compilador não consegue detectar erros. Generics mantêm as informações de tipo fluindo pelo seu código, então você tem flexibilidade e segurança ao mesmo tempo.
Funções Genéricas na Prática
Vejamos um cenário comum: uma função que retorna o primeiro elemento de um array.
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
Sem generics, você poderia escrever function firstElement(arr: any[]): any, o que perde o tipo. Com generics, se você passar um string[], o tipo de retorno é string | undefined.
Você também pode usar múltiplos parâmetros de tipo. Por exemplo, uma função que mapeia um array para um novo array usando um callback:
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
Isso é essencialmente como Array.prototype.map é tipado na biblioteca padrão do TypeScript.
Interfaces Genéricas e Aliases de Tipo
Generics não se limitam a funções. Você pode criar interfaces genéricas e aliases de tipo para definir estruturas de dados reutilizáveis.
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
Isso é útil para respostas de API, gerenciamento de estado ou qualquer tipo de contêiner.
Você também pode fornecer tipos padrão:
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
Agora ApiResponse sem um argumento de tipo usa unknown como padrão.
Restrições Genéricas
Às vezes você quer restringir os tipos que podem ser usados com um generic. Você pode usar a palavra-chave extends para adicionar uma restrição.
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
Agora T deve ter uma propriedade length. Você pode chamar getLength("hello") ou getLength([1,2,3]), mas não getLength(42).
Restrições são poderosas ao trabalhar com objetos que compartilham uma forma comum.
Usando Generics com Classes
Classes também podem ser genéricas. Um exemplo comum é uma coleção genérica:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const popped = stack.pop(); // number | undefined
Isso garante que apenas números possam ser adicionados a uma pilha de números.
Exemplo do Mundo Real: Um Emissor de Eventos com Tipagem Segura
Vamos construir um emissor de eventos simples que usa generics para mapear nomes de eventos para tipos de payload.
type EventMap = Record<string, any>;
class EventEmitter<T extends EventMap> {
private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
this.listeners[event]?.forEach(listener => listener(payload));
}
}
// Uso
interface MyEvents {
login: { userId: string };
logout: { userId: string };
}
const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (payload) => {
console.log(payload.userId); // string
});
emitter.emit('login', { userId: '123' }); // OK
// emitter.emit('login', { userId: 123 }); // Erro
Esse padrão oferece total segurança de tipos para nomes de eventos e payloads, reduzindo erros em tempo de execução.
Armadilhas Comuns e Boas Práticas
- Não use generics em excesso. Se uma função só funciona com um tipo específico, não a torne genérica.
- Use nomes descritivos para parâmetros de tipo.
Té bom para casos simples, masTItemouTResponsepodem ser mais claros. - Prefira restrições para evitar erros em tempo de execução. Se você precisa de uma propriedade, adicione uma restrição.
- Aproveite a inferência de tipos. Muitas vezes você não precisa passar argumentos de tipo explicitamente; o TypeScript consegue inferi-los.
Comparação: Generics vs. any vs. Tipos de União
| Abordagem | Segurança de Tipos | Flexibilidade | Caso de Uso |
|---|---|---|---|
| Generics | Alta | Alta | Componentes reutilizáveis com tipos preservados |
any |
Nenhuma | Alta | Prototipagem rápida, migração de JS |
| Tipos de União | Média | Média | Conjunto limitado de tipos conhecidos |
Generics equilibram flexibilidade e segurança, tornando-os ideais para bibliotecas e utilitários compartilhados.
Perguntas Frequentes
Quando devo usar generics em vez de tipos de união?
Use generics quando a relação de tipo entre entradas e saídas importa. Tipos de união servem para quando um valor pode ser um de um conjunto fixo de tipos, mas o tipo específico não precisa ser preservado entre operações.
Posso usar generics com arrow functions?
Sim, mas a sintaxe pode ser complicada em arquivos .tsx. Você pode escrever const identity = <T,>(arg: T): T => arg; com uma vírgula no final para evitar ambiguidade com JSX.
Como restringir um generic para aceitar apenas objetos?
Use T extends object ou T extends Record<string, unknown> para garantir que T seja um tipo de objeto.
Generics são uma pedra angular do sistema de tipos do TypeScript. Ao usá-los no seu código do dia a dia, você pode escrever funções, interfaces e classes que são ao mesmo tempo flexíveis e seguras. Comece com funções genéricas simples, depois incorpore gradualmente restrições e padrões mais complexos conforme necessário. Para mais ferramentas de desenvolvedor, confira nosso JSON Formatter para validar e formatar seus dados JSON rapidamente.