Voltar para Artigos
Front-end★ Destaque8 min de leitura

TypeScript Avançado: Generics, Utility Types e Componentes React Genéricos

Além do básico <T>: como restringir Generics com extends, usar inferência de tipos (infer), mapeamento de chaves (keyof) e construir componentes React altamente reutilizáveis como Tabelas e Selects.

12 de agosto de 2026

O instinto inicial ao criar uma função que lida com múltiplos tipos é usar any. Isso não só desliga a verificação estática, como destrói o autocomplete (IntelliSense) de quem consome a função. O TypeScript resolve isso com Generics: variáveis de tipo que capturam o formato dos dados passados e o preservam até o retorno.

A diferença entre any e um Generic é fundamental: any diz 'esquece a tipagem, confie em mim'. Um Generic diz 'eu não sei exatamente qual tipo virá, mas o tipo do retorno será o mesmo que o tipo da entrada'. O compilador rastreia essa relação e garante consistência. Se você passar um User, a função retornará dados do tipo User — e a IDE vai saber disso sem nenhuma Type Assertion.

O Básico e Restrições (extends)

Um caso clássico é uma função de paginação ou busca. Se criarmos uma abstração para buscar no banco, queremos garantir que a função saiba exatamente o formato do dado retornado.

Generics Básicos
// O tipo T é injetado na chamada. O retorno será Array<T>.
async function fetchData<T>(url: string): Promise<Array<T>> {
  const response = await fetch(url);
  return response.json();
}

// Ao chamar, cravamos a tipagem. 'users' ganha autocomplete imediato.
const users = await fetchData<User>('/api/users');
console.log(users[0].email);

Mas e se a função precisar ler uma propriedade específica do objeto passado? Você não pode ler item.id se o TypeScript não souber se o tipo genérico T possui um id. Usamos extends para restringir o Generic. Pense em T extends HasId como um contrato: 'aceito qualquer tipo, desde que ele tenha pelo menos a propriedade id'. O tipo pode ter outras propriedades além das exigidas pelo contrato — a restrição é mínima, não exata.

Restrições de Generic
interface HasId {
  id: string;
}

// T pode ser QUALQUER coisa, DESDE QUE obedeça o formato de HasId
function extractIds<T extends HasId>(items: Array<T>): Array<string> {
  return items.map(item => item.id); // Válido, o compilador sabe que .id existe
}

extractIds([{ id: '1', name: 'John' }]); // ✅ Funciona
extractIds([{ name: 'Jane' }]);          // ❌ Erro: propriedade 'id' ausente

Magia Negra: keyof e Mapeamento

O keyof T é um operador que produz um union type de todas as chaves de T. Se T é { id: number, name: string }, então keyof T é 'id' | 'name'. Combinado com Generics, isso permite criar funções que aceitam apenas propriedades que existem de fato no objeto — o TypeScript vai recusar em tempo de compilação se você passar o nome de uma chave que não existe.

Uma função que acessa uma propriedade dinâmica de um objeto pode ser tipada com precisão absoluta usando keyof.

Pluck function
// K deve ser obrigatoriamente o nome de alguma chave existente dentro de T
function pluck<T, K extends keyof T>(items: Array<T>, key: K): Array<T[K]> {
  return items.map(item => item[key]);
}

const users = [
  { id: 1, name: 'John', age: 30 },
  { id: 2, name: 'Jane', age: 25 }
];

// A IDE vai sugerir 'id', 'name', ou 'age'.
// O TS infere inteligentemente que 'names' é um array de strings!
const names = pluck(users, 'name');

// ❌ Erro em tempo de compilação: 'email' não é chave de User
const emails = pluck(users, 'email');

Componentes Genéricos no React

O caso de uso mais poderoso no Frontend é a criação de componentes de UI baseados em dados (como Tabelas ou Selects).

Imagine uma <Table> que funciona com qualquer tipo de dado: usuários, produtos, pedidos. Sem Generics, você criaria uma <UserTable>, uma <ProductTable>, etc. — duplicação massiva. Com Generics, um único componente <Table<T>> recebe os dados tipados e a definição das colunas (incluindo quais chaves do objeto exibir), e o TypeScript garante que você não passe uma chave que não existe no objeto. O accessor: keyof T é o coração disso: ele aceita apenas strings que sejam chaves reais de T.

Table.tsx
import type { ReactNode } from 'react';

// Nossa Tabela aceita qualquer objeto que tenha um id
interface TableProps<T extends { id: string | number }> {
  data: Array<T>;
  // Columns define as chaves (keyof T) e como renderizá-las
  columns: Array<{
    header: string;
    accessor: keyof T | ((item: T) => ReactNode);
  }>;
}

// A sintaxe <T extends ...> antes dos parênteses indica que o componente é genérico
export function Table<T extends { id: string | number }>({ data, columns }: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>{columns.map(col => <th key={col.header}>{col.header}</th>)}</tr>
      </thead>
      <tbody>
        {data.map(item => (
          <tr key={item.id}>
            {columns.map(col => (
              <td key={col.header}>
                {typeof col.accessor === 'function'
                  ? col.accessor(item)
                  : String(item[col.accessor])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

A ergonomia do uso é o que torna os componentes genéricos tão valiosos. O TypeScript infere automaticamente o tipo T a partir da prop data — você não precisa escrever <Table<User>> explicitamente. A partir daí, a prop columns fica totalmente ciente dos campos disponíveis, com autocomplete e erros de compilação se você tentar referenciar uma chave inexistente. A propriedade accessor também aceita uma função (item: T) => ReactNode para células que precisam de renderização customizada, como botões de ação.

Uso da Tabela Genérica
// Ao usar o componente, a inferência é automática baseada na prop 'data'
// A propriedade 'accessor' aceitará APENAS chaves existentes em 'users'
<Table
  data={[{ id: 1, name: 'John', email: 'john@doe.com' }]}
  columns={[
    { header: 'Nome', accessor: 'name' }, // Autocomplete perfeito
    { header: 'E-mail', accessor: 'email' },
    // { header: 'Idade', accessor: 'age' } -> Erro! 'age' não existe.
    { header: 'Ações', accessor: (user) => <button onClick={() => alert(user.id)}>Editar</button> }
  ]}
/>

Conclusão

Generics transferem a responsabilidade da tipagem de quem escreve a função para quem a chama. Compreender extends (restrições) e keyof (mapeamento) permite criar abstrações seguras, substituindo Type Assertions (as User) e any por um código que se documenta e valida sozinho. Componentes React genéricos eliminam duplicação sem sacrificar type safety — uma tabela que funciona com qualquer dado tipado é mais valiosa do que dez tabelas específicas.