programandoai

Constraints, generics em interfaces e casos reais

Nem todo generic aceita qualquer coisa. Às vezes você precisa garantir que T, seja lá o que for, tenha ao menos uma propriedade específica.

Estudar esta lição interativa →

A função primeiro<T> da lição passada aceita literalmente qualquer array, porque nunca tenta acessar nenhuma propriedade específica de T, só devolve o item inteiro. Mas e uma função de busca, que precisa comparar item.id com um id procurado? Se T não tiver nenhuma garantia, o TypeScript recusa item.id, porque, sem mais informação, T pode ser literalmente qualquer coisa, inclusive algo sem essa propriedade.

A solução é uma constraint (restrição): usando extends depois do nome do generic, você exige que, seja lá qual for o tipo real usado, ele tenha pelo menos aquele formato mínimo:

// Constraint com extends: T pode ser qualquer tipo, contanto que tenha,
// no mínimo, um id do tipo number.
interface ProdutoCatalogo { id: number; nome: string }

function buscarPorId<T extends { id: number }>(itens: T[], id: number): T | undefined {
  return itens.find((item) => item.id === id);
}

const catalogoProdutos: ProdutoCatalogo[] = [
  { id: 1, nome: "Mouse" },
  { id: 2, nome: "Teclado" },
];

console.log(buscarPorId(catalogoProdutos, 2));

T extends { id: number } exige que qualquer tipo usado tenha, no mínimo, um id number.

Pense na constraint como o requisito mínimo de uma vaga de emprego: "aceitamos qualquer profissão, contanto que a pessoa saiba dirigir". Você não está travando para uma profissão específica (isso seria perder a generalidade do generic), só exigindo uma habilidade mínima que o trabalho realmente precisa. T extends { id: number } diz: "T pode ser qualquer formato de objeto, contanto que tenha, no mínimo, um id do tipo number".

Generics também funcionam em interfaces e type aliases, e um dos usos mais comuns em produtos reais é um wrapper de resultado: um formato padronizado para representar "ou deu certo, com um dado de algum tipo, ou deu errado, com uma mensagem", sem precisar escrever um tipo novo para cada dado diferente que uma função pode devolver. Repare que ele é a união discriminada do Módulo 6, com o campo ok como discriminante: depois de if (achado.ok), o TypeScript sabe que dados existe, e no else, que existe erro. Um formato com dados? e erro? opcionais permitiria estados sem sentido (sucesso sem dados, erro com dados):

// Tipo genérico: um wrapper de resultado reutilizável para qualquer tipo de dado.
// É a união discriminada do Módulo 6, agora com um T no lugar do dado.
type Resultado<T> =
  | { ok: true; dados: T }
  | { ok: false; erro: string };

function buscarProduto(id: number): Resultado<{ nome: string; preco: number }> {
  if (id === 1) {
    return { ok: true, dados: { nome: "Mouse", preco: 89.9 } };
  }
  return { ok: false, erro: "Produto não encontrado" };
}

const achado = buscarProduto(1);
if (achado.ok) {
  console.log(achado.dados.nome); // aqui o TypeScript sabe que dados existe
} else {
  console.log(achado.erro);
}
console.log(buscarProduto(99));
Sem constraint, o acesso é recusado, não permitido "por precaução"

É comum achar, por engano, que um generic sem restrição (T sozinho) devia "deixar passar" qualquer acesso, já que T pode ser qualquer coisa. É o oposto: exatamente porque T pode ser qualquer coisa, o TypeScript recusa qualquer acesso que não esteja garantido para todos os tipos possíveis. Só depois de uma constraint (extends) o compilador tem uma base mínima para liberar alguma coisa.

Complete a função genérica localizarPorId<T extends { id: number }>(itens: T[], id: number): T | undefined, que devolve o próprio item da lista (não uma cópia) cujo id bate com o procurado, em qualquer posição, ou undefined se nenhum bater (dica: itens.find(...)). Não use eval nem JSON.parse: eles devolvem any e escondem os erros de tipo. É a mesma ideia de buscarPorId, só com outro nome, para não colidir com o exemplo acima.
interface ProdutoLista {
  id: number;
  nome: string;
}

function localizarPorId<T extends { id: number }>(itens: T[], id: number): T | undefined {
  // sua vez: use itens.find para localizar o item com o id procurado
}

const listaProdutos: ProdutoLista[] = [
  { id: 1, nome: "Mouse" },
  { id: 2, nome: "Teclado" },
];

console.log(localizarPorId(listaProdutos, 2));

🧩 Este é um exercício com correção automática — abra no app para completá-lo e verificar.

← Voltar ao índice do curso