Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori5 min de leitura

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:

ts
function devolverNumero(valor: number): number {
  return valor;
}

function devolverTexto(valor: string): string {
  return valor;
}

console.log(devolverNumero(42));
console.log(devolverTexto('pedido'));
42 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:

ts
function devolver(valor: any): any {
  return valor;
}

const codigo = devolver(42);
console.log(codigo.toUpperCase());
TypeError: codigo.toUpperCase is not a function

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:

ts
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());
42.00 PEDIDO

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:

ts
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([]));
AZUL 19.90 undefined

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:

ts
function tamanho<T>(valor: T): number {
  return valor.length;
}
error TS2339: Property 'length' does not exist on type 'T'.

O erro é correto: T poderia ser number. Uma generic constraint limita o conjunto aceito sem escolher um tipo concreto único:

ts
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]));
10 3

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:

error TS2345: Argument of type 'number' is not assignable to parameter of type 'ComComprimento'.

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:

ts
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));
MONITOR 1200.00

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:

error TS2345: Argument of type '"estoque"' is not assignable to parameter of type '"nome" | "preco" | "ativo"'.

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:

ts
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);
}
Mouse

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:

ts
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());
[ { id: 1, nome: 'Ana' } ]

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:

ts
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);
ok 42

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.

ts
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.

  • typescript
  • generics
  • tipos genericos
  • constraints
  • keyof

Perguntas frequentes

O que significa T em um generic?
T é apenas um nome convencional para um parâmetro de tipo. Ele representa um tipo que será inferido ou informado no uso. Você pode usar nomes mais descritivos, como Item ou Resposta.
Generic e any são a mesma coisa?
Não. Any desliga a checagem e perde informação. Um generic aceita tipos variados enquanto preserva relações, por exemplo devolver exatamente o mesmo tipo recebido.
Quando devo usar extends num generic?
Use uma constraint quando a implementação precisa de uma capacidade mínima, como uma propriedade length. O tipo concreto continua preservado, mas só entram valores que cumprem esse requisito.
Preciso informar o tipo entre sinais de menor e maior?
Na maioria das chamadas o TypeScript infere pelo argumento. Informe explicitamente quando não existe valor suficiente para inferir ou quando você precisa escolher um contrato mais amplo que o valor atual.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. TypeScript Handbook — Generics — typescriptlang.org
  2. TypeScript Handbook — Keyof Type Operator — typescriptlang.org
  3. TypeScript Handbook — Creating Types from Types — typescriptlang.org

Continue por aqui