Uso de Tipos Genéricos no TypeScript: Conceitos e Exemplos Práticos

18/09/2024

O Que São Tipos Genéricos no TypeScript?

Tipos genéricos no TypeScript são uma forma de criar componentes reutilizáveis que funcionam com vários tipos de dados, garantindo a segurança de tipos e a flexibilidade. Eles permitem que você defina uma função, classe ou interface que possa operar em diferentes tipos sem perder a verificação de tipos.

Os genéricos são amplamente usados em coleções, estruturas de dados e funções utilitárias, oferecendo uma maneira poderosa de lidar com tipos de forma mais abstrata, mas ainda assim segura.

Conceito de Tipos Genéricos

Os genéricos são definidos usando a sintaxe de "T" ou outra letra representativa. Isso indica que o tipo específico será determinado no momento em que a função, classe ou interface for usada.

Veja um exemplo simples de função genérica:

function identidade(valor: T): T {
  return valor;
}

Essa função identidade aceita um valor do tipo T (qualquer tipo) e retorna esse mesmo valor. O tipo exato de T será inferido com base no argumento passado no momento da chamada.

Exemplo Prático: Função Genérica

Uma função genérica pode ser usada com diferentes tipos de valores, garantindo a tipagem correta para cada chamada.

Exemplo com Diferentes Tipos

const numero = identidade(42);
const texto = identidade('Olá');
const booleano = identidade(true);

console.log(numero);   // Saída: 42
console.log(texto);    // Saída: Olá
console.log(booleano); // Saída: true

Na chamada da função identidade, estamos especificando explicitamente os tipos number, string e boolean. O TypeScript garante que o tipo correto seja inferido e verificado em tempo de compilação.

Usando Genéricos com Arrays

Os tipos genéricos são extremamente úteis quando lidamos com arrays ou coleções de dados. Podemos garantir que todos os elementos do array sejam do mesmo tipo.

Exemplo de Função Genérica com Arrays

function primeiroElemento(array: T[]): T | undefined {
  return array.length > 0 ? array[0] : undefined;
}

const numeros = [1, 2, 3, 4];
const nomes = ['Ana', 'Bruno', 'Carlos'];

console.log(primeiroElemento(numeros)); // Saída: 1
console.log(primeiroElemento(nomes));   // Saída: Ana

A função primeiroElemento recebe um array de qualquer tipo T e retorna o primeiro elemento desse array. O TypeScript garante que o retorno seja do mesmo tipo do array fornecido.

Tipos Genéricos com Interfaces

Interfaces também podem ser genéricas, permitindo que você crie estruturas de dados que funcionam com diferentes tipos.

Exemplo de Interface Genérica

interface Par {
  primeiro: T;
  segundo: U;
}

const coordenadas: Par = { primeiro: 10, segundo: 20 };
const nomeIdade: Par = { primeiro: 'Mauro', segundo: 30 };

console.log(coordenadas); // Saída: { primeiro: 10, segundo: 20 }
console.log(nomeIdade);   // Saída: { primeiro: 'Mauro', segundo: 30 }

A interface Par é genérica e aceita dois tipos diferentes, T e U. Com isso, podemos criar objetos com pares de diferentes tipos, mantendo a verificação de tipos em tempo de compilação.

Classes Genéricas no TypeScript

As classes também podem ser definidas como genéricas, permitindo a criação de estruturas de dados reutilizáveis para diferentes tipos.

Exemplo de Classe Genérica

class Caixa {
  private conteudo: T;

  constructor(conteudo: T) {
    this.conteudo = conteudo;
  }

  obterConteudo(): T {
    return this.conteudo;
  }
}

const caixaNumero = new Caixa(123);
const caixaTexto = new Caixa('Olá');

console.log(caixaNumero.obterConteudo()); // Saída: 123
console.log(caixaTexto.obterConteudo());  // Saída: Olá

A classe Caixa é genérica e aceita qualquer tipo T. Isso permite que a mesma estrutura seja reutilizada para armazenar e manipular diferentes tipos de dados, mantendo a segurança de tipos.

Restrições em Tipos Genéricos

Você pode adicionar restrições aos tipos genéricos usando a palavra-chave extends. Isso garante que o tipo genérico herde de uma interface ou tipo específico, adicionando mais controle à tipagem.

Exemplo com Restrições

interface ComNome {
  nome: string;
}

function exibirNome(objeto: T): void {
  console.log(`Nome: ${objeto.nome}`);
}

const pessoa = { nome: 'João', idade: 25 };
exibirNome(pessoa); // Saída: Nome: João

Neste exemplo, o tipo genérico T é restrito a objetos que implementam a interface ComNome. Isso garante que o objeto passado para a função exibirNome tenha a propriedade nome.

Boas Práticas com Tipos Genéricos

  • Use Genéricos para Criar Código Reutilizável: Genéricos são ideais para criar funções e classes reutilizáveis, que funcionam com diferentes tipos de dados, garantindo a flexibilidade do código.
  • Adicione Restrições Sempre Que Possível: Quando necessário, adicione restrições aos tipos genéricos para garantir que os dados atendam a critérios específicos, mantendo a segurança de tipos.
  • Prefira Inferência de Tipos: O TypeScript pode inferir tipos automaticamente em muitas situações. Prefira deixar que o compilador infira os tipos quando possível, para simplificar o código.
  • Mantenha a Simplicidade: Evite tornar genéricos excessivamente complexos. Mantenha as definições de tipos simples e claras para garantir a legibilidade do código.

Conclusão

Tipos genéricos no TypeScript são uma poderosa ferramenta para criar código flexível e reutilizável, sem sacrificar a segurança de tipos. Eles permitem que você crie funções, classes e interfaces que possam operar em diferentes tipos de dados, garantindo a consistência e previsibilidade. Ao usar genéricos de maneira eficaz, você melhora a manutenibilidade e a clareza do seu código, tornando-o mais escalável e menos propenso a erros.