TypeScript Generics para Código Diário
Você está escrevendo uma função que retorna o primeiro elemento de um array. Você quer que ela funcione para números, strings e objetos, mas também quer que o tipo de retorno corresponda ao tipo de entrada. Sem generics, você recorreria a any e perderia toda a segurança de tipos. É aí que entram os generics do TypeScript.
Generics permitem que você escreva código que funciona com múltiplos tipos enquanto preserva a informação específica do tipo. Neste guia, exploraremos padrões práticos para usar generics no código TypeScript do dia a dia.
O que são Generics?
Generics são uma forma de parametrizar tipos. Assim como funções recebem parâmetros, generics recebem parâmetros de tipo. Eles permitem criar componentes reutilizáveis que funcionam com uma variedade de tipos em vez de um único tipo.
Considere esta função identidade:
function identity<T>(arg: T): T {
return arg;
}
Aqui, T é uma variável de tipo. Quando você chama identity(42), o TypeScript infere T como number. Quando você chama identity("hello"), T se torna string. O tipo de retorno sempre corresponde ao tipo de entrada.
Funções Genéricas na Prática
Vamos ver um cenário comum: uma função que envolve um valor em um objeto.
function wrapInObject<T>(value: T): { value: T } {
return { value };
}
Sem generics, você teria que usar any ou unknown, e os chamadores perderiam a informação de tipo. Com generics, o tipo de retorno é precisamente { value: T }.
Outro exemplo do dia a dia é uma função que retorna o último elemento de um array:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
O TypeScript infere T a partir do argumento do array, então last([1, 2, 3]) retorna number | undefined.
Restrições Genéricas
Às vezes você quer restringir os tipos que podem ser usados com seu generic. Por exemplo, você pode precisar acessar uma propriedade que só existe em certos tipos. Você pode usar a palavra-chave extends para adicionar uma restrição.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
Agora T deve ter uma propriedade length do tipo number. Isso funciona para strings, arrays e qualquer objeto com uma propriedade length.
Você também pode usar restrições com interfaces:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
Isso garante que os itens passados para findById tenham uma propriedade id.
Interfaces e Aliases de Tipo Genéricos
Generics não se limitam a funções. Você pode definir interfaces genéricas e aliases de tipo para criar estruturas de dados flexíveis.
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return { data, status: response.status };
}
Agora você pode tipar respostas de API com precisão:
interface User {
id: number;
name: string;
}
const userResponse = await fetchData<User>('/api/user');
// userResponse.data é User
Classes Genéricas
Classes também podem ser genéricas. Um exemplo comum é uma pilha simples:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined
Generics em Componentes React
Se você está construindo componentes React, generics podem tornar seus componentes mais reutilizáveis. Por exemplo, um componente de lista que renderiza itens de qualquer tipo:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}
Agora você pode usar List com qualquer tipo e obter verificação completa de tipos no callback renderItem.
Armadilhas Comuns e Boas Práticas
- Não exagere no uso de generics. Se uma função só funciona com um tipo específico, use esse tipo. Generics adicionam complexidade.
- Use nomes descritivos para parâmetros de tipo. Embora
Tseja comum, para múltiplos parâmetros use nomes comoTInput,TOutput. - Prefira restrições a
any. Restrições oferecem segurança de tipos enquanto ainda são flexíveis. - Lembre-se de que generics são apagados em tempo de execução. Eles existem apenas em tempo de compilação.
Comparação: Generics vs. any vs. Tipos de União
| Abordagem | Segurança de Tipos | Flexibilidade | Caso de Uso |
|---|---|---|---|
any |
Nenhuma | Alta | Migração de JS, protótipos rápidos |
| Tipos de união | Boa | Limitada a tipos conhecidos | Quando você conhece todos os tipos possíveis |
| Generics | Forte | Alta | Componentes reutilizáveis com tipos preservados |
FAQ
Quando devo usar generics em vez de any?
Use generics sempre que quiser preservar informações de tipo em uma função ou componente. any desabilita a verificação de tipos, enquanto generics a mantêm. Se você se pegar recorrendo a any para fazer algo funcionar, considere se um generic poderia resolver de forma mais segura.
Posso usar múltiplos parâmetros de tipo?
Sim. Você pode definir funções com múltiplos parâmetros de tipo, como function pair<K, V>(key: K, value: V): [K, V]. Use nomes descritivos quando houver mais de um.
Como generics funcionam com arrow functions em arquivos .tsx?
Em arquivos .tsx, <T> pode ser ambíguo com JSX. Use uma vírgula final: const identity = <T,>(arg: T): T => arg; ou use extends unknown: <T extends unknown>.
Generics são um recurso poderoso que pode tornar seu código TypeScript mais reutilizável e seguro em termos de tipos. Comece com funções genéricas simples, depois aplique-as gradualmente a interfaces e classes. Se você precisar formatar ou validar dados JSON com segurança de tipos, confira nossa ferramenta JSON Formatter para inspecionar e formatar seu JSON rapidamente.