Generics no TypeScript: reutilize tipos com segurança
Entenda parâmetros de tipo, inferência, constraints e keyof com funções reutilizáveis que preservam a relação entre entrada e saída no TypeScript.
Generics permitem escrever uma função, interface ou tipo reutilizável sem apagar
o tipo concreto usado em cada chamada. Você declara um parâmetro de tipo, como
T, e o TypeScript liga as posições relacionadas: se entra Produto, sai Produto;
se entra Cliente, sai Cliente.
O ganho aparece quando duas soluções ruins disputam espaço: duplicar a mesma
função para cada tipo ou aceitar any e perder a checagem. Nesta lição, a gente
constrói o generic somente depois de reproduzir esse problema. É a quinta etapa
da trilha de TypeScript, então vale dominar tipos,
objetos e narrowing antes.
A etiqueta que acompanha a caixa: o modelo mental
Imagine um centro de distribuição com caixas padronizadas. A esteira não precisa saber antecipadamente se cada caixa guarda livro, teclado ou caneca. Mas a etiqueta precisa acompanhar o conteúdo: uma caixa marcada como “teclado” deve sair do outro lado ainda identificada como teclado, não como “coisa qualquer”.
No mapa técnico, a caixa reutilizável é a função ou estrutura genérica, o
conteúdo concreto é o argumento e a etiqueta é o parâmetro de tipo. T cria a
relação entre entrada e saída. Uma constraint funciona como requisito da
esteira: “aceito qualquer conteúdo que tenha comprimento”, por exemplo.
O limite da analogia é essencial: generics não inspecionam caixas em runtime nem geram etiquetas no JavaScript. Eles existem na checagem. E nem toda função reutilizável precisa de generic; se entrada e saída não compartilham informação, um tipo normal costuma comunicar melhor.
A duplicação mostra a relação escondida
Sem generic, uma função identidade pode nascer repetida:
function devolverNumero(valor: number): number {
return valor;
}
function devolverTexto(valor: string): string {
return valor;
}
console.log(devolverNumero(42));
console.log(devolverTexto('pedido'));As duas funções têm a mesma regra: devolver o que entrou. Só o tipo muda. Você
poderia substituir ambos por any, mas isso corta a ligação:
function devolver(valor: any): any {
return valor;
}
const codigo = devolver(42);
console.log(codigo.toUpperCase());O compilador permitiu a chamada porque any aceita qualquer operação. A função
era reutilizável, mas a informação de que 42 é número desapareceu. Generic
resolve exatamente essa tensão.
Um parâmetro de tipo preserva a entrada na saída
Coloque <T> antes dos parâmetros da função e use T nas posições relacionadas:
function devolver<T>(valor: T): T {
return valor;
}
const codigo = devolver(42);
const nome = devolver('pedido');
console.log(codigo.toFixed(2));
console.log(nome.toUpperCase());O nome técnico de T é type parameter. Na primeira chamada, o compilador
infere number; na segunda, infere string. O retorno mantém cada escolha. Se
você tentar codigo.toUpperCase(), agora o erro aparece na checagem, porque a
informação não foi apagada.
T não é palavra reservada. Nomes como Item, Entrada e Resposta ficam
melhores quando há vários parâmetros. A convenção curta funciona em relações
pequenas, assim como i funciona num laço curto.
O generic precisa aparecer numa relação útil
Uma função que pega o primeiro item liga o elemento do array ao retorno:
function primeiro<Item>(itens: Item[]): Item | undefined {
return itens[0];
}
const primeiraCor = primeiro(['azul', 'verde']);
const primeiroPreco = primeiro([19.9, 29.9]);
console.log(primeiraCor?.toUpperCase());
console.log(primeiroPreco?.toFixed(2));
console.log(primeiro([]));Item | undefined representa a lista vazia. O parâmetro de tipo aparece na
entrada e na saída, portanto carrega informação entre elas. Compare com uma
função function registrar<T>(mensagem: string): void: se T não participa de
nenhum valor ou relação, quem chama precisa inventar um tipo que a implementação
nem usa. Esse generic provavelmente deve ser removido.
Constraints exigem uma capacidade mínima
Uma função genérica não pode assumir que todo T tem .length:
function tamanho<T>(valor: T): number {
return valor.length;
}O erro é correto: T poderia ser number. Uma generic constraint limita o
conjunto aceito sem escolher um tipo concreto único:
type ComComprimento = {
length: number;
};
function tamanho<T extends ComComprimento>(valor: T): number {
return valor.length;
}
console.log(tamanho('TypeScript'));
console.log(tamanho([10, 20, 30]));extends aqui significa “T precisa ser compatível com ComComprimento”. String e
array têm length; número não. A chamada tamanho(42) reproduz:
A constraint deve representar apenas o que a implementação usa. Exigir um objeto
Produto inteiro para uma função que só lê id diminui a reutilização sem aumentar
segurança.
Keyof liga uma chave ao objeto correto
keyof produz uma union com as chaves conhecidas de um tipo. Com dois parâmetros
genéricos, você pode garantir que a chave pertence ao objeto e preservar o tipo
da propriedade escolhida:
function obter<Objeto, Chave extends keyof Objeto>(
objeto: Objeto,
chave: Chave,
): Objeto[Chave] {
return objeto[chave];
}
const produto = {
nome: 'Monitor',
preco: 1200,
ativo: true,
};
const nome = obter(produto, 'nome');
const preco = obter(produto, 'preco');
console.log(nome.toUpperCase());
console.log(preco.toFixed(2));Chave só pode ser 'nome' | 'preco' | 'ativo'. O retorno Objeto[Chave] é um
indexed access type: o tipo localizado naquela propriedade. Ao chamar
obter(produto, 'estoque'), o compilador mostra:
Sem a relação genérica, uma função com chave string aceitaria erros de digitação
e devolveria algo amplo demais. Para entender por que a forma do objeto é
suficiente, revise interface vs type.
Tipos e interfaces também recebem parâmetros
Uma resposta de operação pode carregar dados diferentes, mantendo a mesma estrutura ao redor:
type Resultado<Dado> =
| { sucesso: true; dado: Dado }
| { sucesso: false; erro: string };
type Produto = {
id: number;
nome: string;
};
const resultado: Resultado<Produto> = {
sucesso: true,
dado: { id: 1, nome: 'Mouse' },
};
if (resultado.sucesso) {
console.log(resultado.dado.nome);
}Resultado<Dado> é um generic aplicado a um type alias. A union discriminada
impede ler dado quando sucesso é falso, e o parâmetro preserva qual dado
existe no caso positivo. O mesmo envelope serve para Produto, Cliente ou uma
lista sem recorrer a any.
Interfaces também podem ser genéricas:
interface Repositorio<Entidade> {
salvar(item: Entidade): void;
listar(): Entidade[];
}
type Cliente = { id: number; nome: string };
const clientes: Cliente[] = [];
const repositorio: Repositorio<Cliente> = {
salvar(item) {
clientes.push(item);
},
listar() {
return clientes;
},
};
repositorio.salvar({ id: 1, nome: 'Ana' });
console.log(repositorio.listar());Aqui o tipo é informado ao criar o repositório, porque todas as operações da instância compartilham a mesma entidade. Em uma função isolada, a inferência costuma resolver na própria chamada.
Um valor padrão reduz argumentos repetidos
Parâmetros de tipo podem ter um default. Isso é útil quando existe uma escolha comum, mas quem usa ainda pode substituí-la:
type Resposta<Conteudo = string> = {
status: number;
conteudo: Conteudo;
};
const texto: Resposta = { status: 200, conteudo: 'ok' };
const contagem: Resposta<number> = { status: 200, conteudo: 42 };
console.log(texto.conteudo, contagem.conteudo);O default não é fallback de runtime. Ele só escolhe um tipo quando o argumento não foi fornecido ou inferido. Se quase todo uso precisa substituir o padrão, talvez ele esteja escondendo uma decisão em vez de poupar ruído.
Quando não criar um generic
Use generic quando uma relação precisa sobreviver. Não use apenas para deixar a
assinatura parecer avançada. Se a função sempre recebe string e devolve number,
(texto: string) => number é mais precisa que uma combinação de parâmetros de
tipo. Se você só precisa de uma capacidade, uma interface concreta pode bastar.
Minha revisão faz três perguntas: o parâmetro aparece em pelo menos duas posições
relacionadas? Quem chama consegue inferir ou escolher algo significativo? A
implementação respeita qualquer tipo permitido pela constraint? Se as respostas
forem “não”, simplifico antes de adicionar outro T.
Missão: preserve o item encontrado
Escreva buscarPorId<Item extends { id: number }>(itens: Item[], id: number): Item | undefined. Teste com produtos que possuem preço e clientes que possuem
email. O retorno de produtos deve permitir .preco; o de clientes, .email.
function buscarPorId<Item extends { id: number }>(
itens: Item[],
id: number,
): Item | undefined {
return itens.find((item) => item.id === id);
}O critério de sucesso tem quatro provas: as duas listas usam a mesma função; cada
retorno preserva suas propriedades; uma lista de strings falha pela constraint;
e o caso não encontrado é tratado antes do acesso. Não use any nem faça
assertion com as.
Ao terminar, volte ao guia de TypeScript e compare a solução com a tabela de decisão. Generic não encerra a trilha porque é o recurso “mais avançado”; ele encerra este cluster porque depende das relações construídas por tipos, objetos e narrowing.
Perguntas frequentes
O que significa T em um generic?
Generic e any são a mesma coisa?
Quando devo usar extends num generic?
Preciso informar o tipo entre sinais de menor e maior?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 com TypeScript 7.0.2, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- TypeScript Handbook — Generics — typescriptlang.org
- TypeScript Handbook — Keyof Type Operator — typescriptlang.org
- TypeScript Handbook — Creating Types from Types — typescriptlang.org


