7 min read

Desvendando os Generics no TypeScript: Um Guia para Iniciantes

Table of Contents

Se vocĂȘ estĂĄ aprendendo TypeScript, jĂĄ deve ter ouvido falar sobre “Generics”. O nome pode parecer complicado, mas a ideia por trĂĄs Ă© muito poderosa e, na verdade, bastante intuitiva. Generics sĂŁo uma das ferramentas mais legais do TypeScript para escrever cĂłdigo flexĂ­vel, seguro e reutilizĂĄvel.

Neste guia, vamos desvendar o que sĂŁo Generics passo a passo, com exemplos prĂĄticos e analogias para que vocĂȘ perca o medo e comece a usĂĄ-los hoje mesmo.

O Problema: CĂłdigo Repetitivo e Inseguro

Imagine que vocĂȘ precisa criar uma função simples que recebe um array e retorna o primeiro elemento.

Se o array for de nĂșmeros, vocĂȘ faria algo assim:

function primeiroElementoNumero(array: number[]): number | undefined {
  return array[0];
}

E se o array for de textos (strings)? VocĂȘ teria que criar outra função:

function primeiroElementoString(array: string[]): string | undefined {
  return array[0];
}

Veja o problema? Estamos repetindo o mesmo código só para mudar o tipo. Uma primeira solução seria usar o tipo any, que aceita qualquer coisa:

// A solução com 'any' (mas é uma må ideia!)
function primeiroElementoQualquer(array: any[]): any {
  return array[0];
}

const primeiroNome = primeiroElementoQualquer(["Ana", "JoĂŁo"]); // OK
primeiroNome.toUpperCase(); 
// O TypeScript nĂŁo sabe que isso Ă© uma string! Ele nĂŁo ajuda.

Ao usar any, perdemos toda a segurança e o autocompletar que o TypeScript nos oferece. É aqui que os Generics entram para salvar o dia!

A Solução: Criando uma “Função-Molde” com Generics

Generics permitem criar um “molde” de função, classe ou interface. Em vez de definir um tipo fixo (como number ou string), vocĂȘ usa um “parĂąmetro de tipo”, que funciona como uma variĂĄvel para o tipo. A convenção Ă© usar a letra T (de “Type”).

Vamos reescrever nossa função primeiroElemento usando Generics:

// Nossa primeira função genérica!
function primeiroElemento<T>(array: T[]): T | undefined {
  return array[0];
}

Vamos quebrar o que aconteceu aqui:

  1. <T>: Aqui, estamos dizendo: “Esta função vai usar um tipo genĂ©rico que eu vou chamar de T”.
  2. array: T[]: O argumento array será um array de
 T! Seja qual for o tipo que T se torne.
  3. : T | undefined: A função retornarå um valor do mesmo tipo T (ou undefined se o array estiver vazio).

Agora, veja a mĂĄgica acontecer:

const numeros = [1, 2, 3, 4, 5];
const primeiroNum = primeiroElemento(numeros); 
// TypeScript sabe que 'primeiroNum' Ă© do tipo number

const nomes = ["Ana", "JoĂŁo", "Maria"];
const primeiroNome = primeiroElemento(nomes); 
// TypeScript sabe que 'primeiroNome' Ă© do tipo string

// Agora o autocompletar funciona!
console.log(primeiroNome.toUpperCase()); 
// "ANA" - O editor sugere métodos de string!

Com uma Ășnica função, resolvemos o problema para todos os tipos de array, mantendo nosso cĂłdigo seguro e inteligente!

Criando “Caixas” FlexĂ­veis: Interfaces GenĂ©ricas

VocĂȘ tambĂ©m pode usar Generics com interfaces para criar “moldes” de objetos. Isso Ă© super comum ao trabalhar com respostas de APIs.

Imagine que uma API sempre retorna um objeto com um campo dados, mas o conteĂșdo desses dados pode variar.

// Uma interface genérica para respostas de API
interface RespostaAPI<TipoDosDados> {
  dados: TipoDosDados;
  sucesso: boolean;
  timestamp: number;
}

// Exemplo 1: Resposta com dados de um usuĂĄrio
interface Usuario {
  id: number;
  nome: string;
}

const respostaUsuario: RespostaAPI<Usuario> = {
  dados: { id: 1, nome: "JoĂŁo" },
  sucesso: true,
  timestamp: 1672531200
};

// Exemplo 2: Resposta com uma lista de produtos
const respostaProdutos: RespostaAPI<string[]> = {
  dados: ["Notebook", "Mouse", "Teclado"],
  sucesso: true,
  timestamp: 1672531201
};

Usamos TipoDosDados para ser mais descritivo que T. VocĂȘ pode nomear seus parĂąmetros de tipo como quiser!

Criando “Estruturas” ReutilizĂĄveis: Classes GenĂ©ricas

Generics tambĂ©m brilham em classes. Vamos criar uma classe Pilha (Stack), uma estrutura de dados onde o Ășltimo item a entrar Ă© o primeiro a sair (como uma pilha de pratos).

class Pilha<TipoDoItem> {
  private items: TipoDoItem[] = [];

  // Adiciona um item no topo da pilha
  push(item: TipoDoItem): void {
    this.items.push(item);
  }

  // Remove e retorna o item do topo da pilha
  pop(): TipoDoItem | undefined {
    return this.items.pop();
  }
}

// Criando uma pilha de nĂșmeros
const pilhaDeNumeros = new Pilha<number>();
pilhaDeNumeros.push(10);
pilhaDeNumeros.push(20);
console.log(pilhaDeNumeros.pop()); // SaĂ­da: 20

// Criando uma pilha de textos
const pilhaDeTextos = new Pilha<string>();
pilhaDeTextos.push("OlĂĄ");
pilhaDeTextos.push("Mundo");
console.log(pilhaDeTextos.pop()); // SaĂ­da: "Mundo"

A mesma classe Pilha funciona perfeitamente para qualquer tipo de dado, evitando duplicação e mantendo a segurança de tipos.

Colocando “Regras”: Constraints (RestriçÔes)

Às vezes, vocĂȘ nĂŁo quer que sua função genĂ©rica aceite qualquer tipo. VocĂȘ pode querer que ela aceite apenas tipos que sigam certas “regras”. Isso Ă© chamado de constraint (restrição).

Imagine uma função que busca um item em um array pelo seu id. A função só faz sentido se os itens do array tiverem uma propriedade id.

// 1. Primeiro, definimos a "regra": o objeto DEVE ter um 'id' do tipo number
interface TemId {
  id: number;
}

// 2. Usamos 'extends' para aplicar a regra ao nosso Generic
function buscarPorId<T extends TemId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

// Exemplo de uso:
const produtos = [
  { id: 1, nome: "Notebook", preco: 2000 },
  { id: 2, nome: "Mouse", preco: 50 }
];

const produtoEncontrado = buscarPorId(produtos, 1);// Funciona! Os produtos tĂȘm 'id'

const configuracoes = [
  { nome: "dark_mode", valor: true }
];

//const configEncontrada = buscarPorId(configuracoes,0);//ERRO! Os itens sem 'id'

A palavra-chave extends garante que o tipo T seja, no mĂ­nimo, compatĂ­vel com a interface TemId.

Por que vocĂȘ vai amar usar Generics?

  1. Escreva menos cĂłdigo: Crie componentes que funcionam para mĂșltiplos tipos, evitando o famoso “copia e cola”.
  2. Escreva cĂłdigo mais seguro: O TypeScript verifica os tipos para vocĂȘ, pegando erros antes mesmo de vocĂȘ rodar o cĂłdigo. Chega de cannot read property 'toUpperCase' of undefined!
  3. Código mais inteligente: Seu editor (como o VS Code) vai te dar sugestÔes e autocompletar muito mais precisos, porque ele sabe exatamente qual tipo estå sendo usado.
  4. Flexibilidade: VocĂȘ cria “ferramentas” (funçÔes, classes) que podem ser adaptadas para diferentes necessidades sem perder a robustez.

ConclusĂŁo

Generics podem parecer um conceito avançado, mas, como vimos, eles resolvem um problema muito comum: como escrever código que seja ao mesmo tempo flexível e seguro.

Não se preocupe se tudo não fez sentido de primeira. A melhor forma de aprender é praticando. Tente criar sua própria função genérica ou uma interface genérica para um projeto seu.

O mais importante Ă© entender a ideia principal: Generics sĂŁo como “variĂĄveis para tipos”, permitindo que vocĂȘ crie blocos de cĂłdigo que sĂŁo verdadeiros canivetes suíços: Ășteis para muitas situaçÔes e sempre muito seguros.