Generics em TypeScript: quando são úteis e quando são ginástica
Há uma ideia errada sobre genéricos que explica quase todo o mau uso deles: a de que servem para aceitar qualquer coisa. Não servem. Para aceitar qualquer coisa há o unknown, e é mais honesto.
Um genérico serve para ligar tipos uns aos outros — dizer que a saída desta função depende do que lhe entrou, e que essas duas coisas são a mesma.
1. A regra que decide quase tudo
É esta, e resolve a maior parte das dúvidas sem discussão: se o parâmetro de tipo aparece uma só vez na assinatura, não está a ligar nada.
// O T aparece uma vez. Não liga a entrada a saída nenhuma,
// e podia ser simplesmente unknown[].
function contar<T>(lista: T[]): number {
return lista.length;
}
// Aqui aparece duas vezes, e é isso que o justifica:
// o que sai é do mesmo tipo do que entrou.
function primeiro<T>(lista: T[]): T | undefined {
return lista[0];
}Antes de escrever <T>, vale a pena contar as ocorrências. Uma só, e o genérico está a enfeitar.
2. O caso que mais se encontra: o invólucro
É o primeiro dos dois genéricos que existem neste site. Todas as consultas públicas passam por uma função que lhes acrescenta cache:
const cacheada = <Args extends unknown[], R>(
fn: (...args: Args) => Promise<R>,
chave: string,
etiquetas: string[],
) => unstable_cache(fn, [chave], { tags: etiquetas, revalidate: REVALIDACAO });O que os dois parâmetros fazem é manter a assinatura intacta. A função que entra recebe uns argumentos e devolve uma promessa de alguma coisa; a que sai recebe os mesmos argumentos e devolve a mesma coisa.
Sem isso, cada consulta embrulhada perdia os tipos, e todos os sítios que a chamam passavam a trabalhar às escuras. O genérico aqui não acrescenta poder nenhum — serve para o invólucro ser invisível, que é exatamente o que se quer de um invólucro.
3. O outro caso: um componente para várias formas
A tabela da administração é a mesma para as frases, para os artigos e para as etiquetas — três registos com colunas completamente diferentes.
type Registo = { id: number } & Record<string, string | number>;
export function TabelaAdmin<R extends Registo>({
registos,
colunas,
}: {
registos: R[];
colunas: Coluna[];
}) { /* ... */ }O que está a trabalhar ali não é o R — é o extends Registo. A restrição diz duas coisas ao mesmo tempo: qualquer forma serve, desde que tenha um id e valores que consigam atravessar para o cliente.
É essa a parte que costuma faltar. Um <T> sem restrição significa que não se sabe nada sobre T — e, não sabendo nada, também não se lhe pode fazer nada.
4. Quando é ginástica
Um genérico com um só sítio de uso. Se só há uma forma a passar por ali, escreva-se essa forma. O genérico é uma promessa de variedade que nunca se cumpre.
Um genérico para evitar uma união. Duas hipóteses conhecidas não são um genérico, são
A | B.Tipos condicionais e
inferpara resolver o que duas sobrecargas resolviam. A versão inteligente é mais curta e leva um dia a ser lida outra vez.
5. O teste que uso
Passar o rato por cima da chamada, no editor, e ler o que ele mostra. Não a definição — a chamada.
const artigo = primeiro(artigos);
// const artigo: Artigo | undefined <- ganhou qualquer coisa
const x = transformar(dados);
// const x: Resultado<Entrada<T>, Chaves<T>> extends ... <- perdeuSe o que aparece é um tipo que alguém consegue ler, o genérico está a pagar o que custa. Se é uma parede de sinais, passou a ser um problema de quem vier a seguir — e esse alguém é, quase sempre, nós próprios daqui a três meses.
6. Duas coisas pequenas que ajudam
Deixar o TypeScript inferir, em vez de escrever o tipo na chamada:
primeiro<Artigo>(artigos); // desnecessário
primeiro(artigos); // ele já sabeE dar nomes aos parâmetros de tipo quando são mais do que um. O T passa; o T, U, V numa assinatura de cinco linhas não passa — Args e R custam o mesmo a escrever e dizem o que são.
O resumo
Um genérico justifica-se quando liga tipos: o que sai depende do que entra, e o compilador tem de saber disso. Se não liga nada, é decoração — e decoração em assinaturas cobra-se na primeira vez que alguém tenta perceber o que aquilo devolve.