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.
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.
// 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.
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' ausenteMagia 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.
// 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.
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.
// 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.