TypeScript

Componentes Genéricos em React com TypeScript: Do Básico ao Avançado

15 min de leitura

Componentes Genéricos em React com TypeScript: Do Básico ao Avançado

O que são Componentes Genéricos em React com TypeScript? Componentes genéricos são componentes React que aceitam tipos genéricos como parâmetros, permitindo que você crie componentes reutilizáveis e type-safe. Quando você trabalha com TypeScript em React, essa capacidade se torna extremamente poderosa porque você consegue definir componentes que funcionam com múltiplos tipos de dados mantendo segurança total em tempo de compilação. Imagine que você precisa criar um componente de lista que funcione tanto para listas de usuários, produtos, comentários ou qualquer outra coisa. Sem genéricos, você teria que recriar esse componente várias vezes ou usar para contornar o problema. Com genéricos, você escreve uma única implementação que funciona perfeitamente para todos os casos. A sintaxe é simples: você usa (ou qualquer letra/nome) para representar um tipo que será definido quando o componente for utilizado. Fundamentos de Genéricos em TypeScript Genéricos Básicos Antes de aplicar genéricos em componentes React, é importante entender como funcionam em TypeScript puro. Genéricos permitem que você escreva

<h2>O que são Componentes Genéricos em React com TypeScript?</h2>

<p>Componentes genéricos são componentes React que aceitam tipos genéricos como parâmetros, permitindo que você crie componentes reutilizáveis e type-safe. Quando você trabalha com TypeScript em React, essa capacidade se torna extremamente poderosa porque você consegue definir componentes que funcionam com múltiplos tipos de dados mantendo segurança total em tempo de compilação.</p>

<p>Imagine que você precisa criar um componente de lista que funcione tanto para listas de usuários, produtos, comentários ou qualquer outra coisa. Sem genéricos, você teria que recriar esse componente várias vezes ou usar <code>any</code> para contornar o problema. Com genéricos, você escreve uma única implementação que funciona perfeitamente para todos os casos. A sintaxe é simples: você usa <code>&lt;T&gt;</code> (ou qualquer letra/nome) para representar um tipo que será definido quando o componente for utilizado.</p>

<h2>Fundamentos de Genéricos em TypeScript</h2>

<h3>Genéricos Básicos</h3>

<p>Antes de aplicar genéricos em componentes React, é importante entender como funcionam em TypeScript puro. Genéricos permitem que você escreva código que funcione com múltiplos tipos enquanto mantém a informação de tipo intacta. Você define um parâmetro de tipo entre colchetes angulares e o usa dentro da sua função ou classe.</p>

<pre><code class="language-typescript">// Função genérica simples

function identidade&lt;T&gt;(valor: T): T {

return valor;

}

const numeroIdentidade = identidade&lt;number&gt;(42); // tipo: number

const stringIdentidade = identidade&lt;string&gt;(&quot;olá&quot;); // tipo: string

// O TypeScript infere o tipo automaticamente

const booleanoIdentidade = identidade(true); // tipo: boolean (inferido)</code></pre>

<p>O poder dos genéricos aparece quando você trabalha com estruturas de dados. Vamos considerar um exemplo prático onde criamos uma classe para gerenciar uma lista de qualquer tipo:</p>

<pre><code class="language-typescript">class Repositorio&lt;T&gt; {

private items: T[] = [];

adicionar(item: T): void {

this.items.push(item);

}

obter(indice: number): T {

return this.items[indice];

}

listar(): T[] {

return this.items;

}

}

// Usando o repositório para diferentes tipos

const repoUsuarios = new Repositorio&lt;{ id: number; nome: string }&gt;();

repoUsuarios.adicionar({ id: 1, nome: &quot;João&quot; });

const repoProdutos = new Repositorio&lt;{ id: number; preco: number }&gt;();

repoProdutos.adicionar({ id: 1, preco: 99.99 });</code></pre>

<h3>Restrições de Genéricos</h3>

<p>Nem sempre você quer aceitar qualquer tipo. TypeScript permite que você restrinja quais tipos podem ser passados como genéricos usando a palavra-chave <code>extends</code>. Isso é essencial para garantir que seu componente receba tipos que possuem as propriedades necessárias.</p>

<pre><code class="language-typescript">// Apenas tipos que têm a propriedade &#039;id&#039;

interface Identificavel {

id: number;

}

class RepositorioSeguro&lt;T extends Identificavel&gt; {

private items: Map&lt;number, T&gt; = new Map();

salvar(item: T): void {

this.items.set(item.id, item);

}

obter(id: number): T | undefined {

return this.items.get(id);

}

}

// Isso funciona

const repo = new RepositorioSeguro&lt;{ id: number; nome: string }&gt;();

// Isso gera erro em tempo de compilação

// const repoInvalido = new RepositorioSeguro&lt;{ nome: string }&gt;();

// Erro: Type does not satisfy constraint Identificavel</code></pre>

<h2>Componentes Genéricos em React</h2>

<h3>Componentes Funcionais Genéricos</h3>

<p>A forma mais moderna de criar componentes genéricos em React é usando componentes funcionais com TypeScript. Você define o tipo genérico entre colchetes angulares e o usa tanto nas props quanto no retorno do componente.</p>

<pre><code class="language-typescript">import React, { ReactNode } from &#039;react&#039;;

// Componente genérico simples

interface ListaProps&lt;T&gt; {

items: T[];

renderItem: (item: T, indice: number) =&gt; ReactNode;

titulo?: string;

}

function Lista&lt;T&gt;({ items, renderItem, titulo }: ListaProps&lt;T&gt;) {

return (

&lt;div className=&quot;lista&quot;&gt;

{titulo &amp;&amp; &lt;h2&gt;{titulo}&lt;/h2&gt;}

&lt;ul&gt;

{items.map((item, indice) =&gt; (

&lt;li key={indice}&gt;{renderItem(item, indice)}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}

export default Lista;</code></pre>

<p>Esse componente é extremamente flexível. Você pode usá-lo com qualquer tipo de dado desde que forneça uma função para renderizar cada item:</p>

<pre><code class="language-typescript">// Usando com usuários

interface Usuario {

id: number;

nome: string;

email: string;

}

const usuarios: Usuario[] = [

{ id: 1, nome: &quot;Ana&quot;, email: &quot;ana@email.com&quot; },

{ id: 2, nome: &quot;Bruno&quot;, email: &quot;bruno@email.com&quot; }

];

// Renderizar a lista de usuários

&lt;Lista&lt;Usuario&gt;

items={usuarios}

titulo=&quot;Usuários do Sistema&quot;

renderItem={(usuario) =&gt; ${usuario.nome} (${usuario.email})}

/&gt;

// Usando com números

const numeros = [1, 2, 3, 4, 5];

&lt;Lista&lt;number&gt;

items={numeros}

titulo=&quot;Números Importantes&quot;

renderItem={(num) =&gt; Número: ${num}}

/&gt;</code></pre>

<h3>Componentes com Múltiplos Genéricos</h3>

<p>Às vezes você precisa de mais de um tipo genérico em um componente. Isso é totalmente válido e muito útil para cenários complexos:</p>

<pre><code class="language-typescript">import React from &#039;react&#039;;

interface PaginacaoProps&lt;T, K&gt; {

items: T[];

chaveId: K;

renderizar: (item: T) =&gt; React.ReactNode;

itensPorPagina?: number;

}

function ComponentePaginado&lt;T extends Record&lt;string, any&gt;, K extends keyof T&gt;({

items,

chaveId,

renderizar,

itensPorPagina = 5

}: PaginacaoProps&lt;T, K&gt;) {

const [paginaAtual, setPaginaAtual] = React.useState(0);

const inicio = paginaAtual * itensPorPagina;

const fim = inicio + itensPorPagina;

const itemsPaginados = items.slice(inicio, fim);

const totalPaginas = Math.ceil(items.length / itensPorPagina);

return (

&lt;div className=&quot;paginado&quot;&gt;

&lt;div className=&quot;items&quot;&gt;

{itemsPaginados.map((item) =&gt; (

&lt;div key={String(item[chaveId])} className=&quot;item&quot;&gt;

{renderizar(item)}

&lt;/div&gt;

))}

&lt;/div&gt;

&lt;div className=&quot;paginacao&quot;&gt;

&lt;button

disabled={paginaAtual === 0}

onClick={() =&gt; setPaginaAtual(p =&gt; p - 1)}

&gt;

Anterior

&lt;/button&gt;

&lt;span&gt;

Página {paginaAtual + 1} de {totalPaginas}

&lt;/span&gt;

&lt;button

disabled={paginaAtual &gt;= totalPaginas - 1}

onClick={() =&gt; setPaginaAtual(p =&gt; p + 1)}

&gt;

Próxima

&lt;/button&gt;

&lt;/div&gt;

&lt;/div&gt;

);

}

// Uso

interface Produto {

id: number;

nome: string;

preco: number;

}

const produtos: Produto[] = [

{ id: 1, nome: &quot;Notebook&quot;, preco: 3000 },

{ id: 2, nome: &quot;Mouse&quot;, preco: 50 },

{ id: 3, nome: &quot;Teclado&quot;, preco: 150 },

// ... mais produtos

];

&lt;ComponentePaginado&lt;Produto, &#039;id&#039;&gt;

items={produtos}

chaveId=&quot;id&quot;

renderizar={(produto) =&gt; (

&lt;div&gt;

&lt;h4&gt;{produto.nome}&lt;/h4&gt;

&lt;p&gt;R$ {produto.preco.toFixed(2)}&lt;/p&gt;

&lt;/div&gt;

)}

itensPorPagina={3}

/&gt;</code></pre>

<h3>Componentes Genéricos com Contexto</h3>

<p>Para casos mais avançados, você pode combinar genéricos com Context API para criar soluções elegantes e reutilizáveis:</p>

<pre><code class="language-typescript">import React, { createContext, useContext, useState, ReactNode } from &#039;react&#039;;

// Criando um contexto genérico para gerenciar qualquer tipo de lista

interface ContextoLista&lt;T&gt; {

items: T[];

adicionar: (item: T) =&gt; void;

remover: (indice: number) =&gt; void;

atualizar: (indice: number, item: T) =&gt; void;

limpar: () =&gt; void;

}

// Factory function para criar um contexto genérico

function criarContextoLista&lt;T&gt;() {

const contexto = createContext&lt;ContextoLista&lt;T&gt; | undefined&gt;(undefined);

function ProvedorLista({ children }: { children: ReactNode }) {

const [items, setItems] = useState&lt;T[]&gt;([]);

const valor: ContextoLista&lt;T&gt; = {

items,

adicionar: (item) =&gt; setItems(prev =&gt; [...prev, item]),

remover: (indice) =&gt; setItems(prev =&gt; prev.filter((_, i) =&gt; i !== indice)),

atualizar: (indice, item) =&gt; {

setItems(prev =&gt; {

const nova = [...prev];

nova[indice] = item;

return nova;

});

},

limpar: () =&gt; setItems([])

};

return (

&lt;contexto.Provider value={valor}&gt;

{children}

&lt;/contexto.Provider&gt;

);

}

function usarLista(): ContextoLista&lt;T&gt; {

const ctx = useContext(contexto);

if (!ctx) {

throw new Error(&#039;usarLista deve ser usado dentro de ProvedorLista&#039;);

}

return ctx;

}

return { ProvedorLista, usarLista };

}

// Criando um contexto específico para tarefas

interface Tarefa {

id: number;

descricao: string;

concluida: boolean;

}

const { ProvedorLista: ProvedorTarefas, usarLista: usarTarefas } =

criarContextoLista&lt;Tarefa&gt;();

// Componente que usa o contexto

function ListaTarefas() {

const { items, adicionar, remover } = usarTarefas();

const [descricao, setDescricao] = useState(&#039;&#039;);

const handleAdicionar = () =&gt; {

if (descricao.trim()) {

adicionar({

id: Date.now(),

descricao,

concluida: false

});

setDescricao(&#039;&#039;);

}

};

return (

&lt;div&gt;

&lt;input

value={descricao}

onChange={(e) =&gt; setDescricao(e.target.value)}

placeholder=&quot;Nova tarefa...&quot;

/&gt;

&lt;button onClick={handleAdicionar}&gt;Adicionar&lt;/button&gt;

&lt;ul&gt;

{items.map((tarefa, indice) =&gt; (

&lt;li key={tarefa.id}&gt;

{tarefa.descricao}

&lt;button onClick={() =&gt; remover(indice)}&gt;Remover&lt;/button&gt;

&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}

// Uso

export default function App() {

return (

&lt;ProvedorTarefas&gt;

&lt;ListaTarefas /&gt;

&lt;/ProvedorTarefas&gt;

);

}</code></pre>

<h2>Padrões Avançados e Boas Práticas</h2>

<h3>Genéricos com Constantes de Tipo</h3>

<p>TypeScript 3.4 introduziu <code>as const</code> que permite trabalhar com tipos literais. Isso é especialmente útil em componentes genéricos quando você quer manter informações específicas de tipo:</p>

<pre><code class="language-typescript">interface Opcao&lt;T extends readonly string[]&gt; {

valores: T;

selecionado?: T[number];

onChange?: (valor: T[number]) =&gt; void;

}

function Dropdown&lt;const T extends readonly string[]&gt;({

valores,

selecionado,

onChange

}: Opcao&lt;T&gt;) {

return (

&lt;select value={selecionado} onChange={(e) =&gt; onChange?.(e.target.value as T[number])}&gt;

{valores.map((valor) =&gt; (

&lt;option key={valor} value={valor}&gt;

{valor}

&lt;/option&gt;

))}

&lt;/select&gt;

);

}

// Tipo é inferido como [&quot;ativo&quot;, &quot;inativo&quot;, &quot;pendente&quot;]

const opcoes = [&quot;ativo&quot;, &quot;inativo&quot;, &quot;pendente&quot;] as const;

&lt;Dropdown

valores={opcoes}

selecionado=&quot;ativo&quot;

onChange={(valor) =&gt; {

// valor é inferido como &quot;ativo&quot; | &quot;inativo&quot; | &quot;pendente&quot;

console.log(valor);

}}

/&gt;</code></pre>

<h3>Condicionais de Tipo</h3>

<p>Para cenários mais complexos, você pode usar tipos condicionais para alterar o comportamento baseado no tipo genérico:</p>

<pre><code class="language-typescript">// Tipo condicional: se T é array, retorna o tipo do elemento, senão retorna T

type ElementoOuValor&lt;T&gt; = T extends (infer E)[] ? E : T;

interface Comparador&lt;T&gt; {

items: T[];

comparar: (a: ElementoOuValor&lt;T&gt;, b: ElementoOuValor&lt;T&gt;) =&gt; number;

renderizar: (item: ElementoOuValor&lt;T&gt;) =&gt; React.ReactNode;

}

function ListaOrdenada&lt;T&gt;({

items,

comparar,

renderizar

}: Comparador&lt;T&gt;) {

const itemsOrdenados = [...items].sort(comparar);

return (

&lt;ul&gt;

{itemsOrdenados.map((item, idx) =&gt; (

&lt;li key={idx}&gt;{renderizar(item)}&lt;/li&gt;

))}

&lt;/ul&gt;

);

}

// Usando com array de números

&lt;ListaOrdenada

items={[3, 1, 4, 1, 5, 9]}

comparar={(a, b) =&gt; a - b}

renderizar={(num) =&gt; Número: ${num}}

/&gt;

// Usando com array de usuários

interface Usuario {

nome: string;

idade: number;

}

&lt;ListaOrdenada

items={[{ nome: &quot;Ana&quot;, idade: 25 }, { nome: &quot;Bruno&quot;, idade: 20 }]}

comparar={(a, b) =&gt; a.idade - b.idade}

renderizar={(usuario) =&gt; ${usuario.nome} - ${usuario.idade} anos}

/&gt;</code></pre>

<h3>Injeção de Dependências com Genéricos</h3>

<p>Para aplicações maiores, você pode criar padrões de injeção de dependência usando genéricos:</p>

<pre><code class="language-typescript">interface Servico&lt;T&gt; {

obter: () =&gt; Promise&lt;T[]&gt;;

criar: (item: T) =&gt; Promise&lt;T&gt;;

deletar: (id: number) =&gt; Promise&lt;void&gt;;

}

interface ProvedorServicoProps&lt;T&gt; {

servico: Servico&lt;T&gt;;

children: (estado: {

dados: T[];

carregando: boolean;

erro: Error | null;

criar: (item: T) =&gt; Promise&lt;void&gt;;

deletar: (id: number) =&gt; Promise&lt;void&gt;;

}) =&gt; React.ReactNode;

}

function ProvedorServico&lt;T&gt;({ servico, children }: ProvedorServicoProps&lt;T&gt;) {

const [dados, setDados] = React.useState&lt;T[]&gt;([]);

const [carregando, setCarregando] = React.useState(true);

const [erro, setErro] = React.useState&lt;Error | null&gt;(null);

React.useEffect(() =&gt; {

servico.obter()

.then(setDados)

.catch(setErro)

.finally(() =&gt; setCarregando(false));

}, [servico]);

const criar = async (item: T) =&gt; {

const novo = await servico.criar(item);

setDados(prev =&gt; [...prev, novo]);

};

const deletar = async (id: number) =&gt; {

await servico.deletar(id);

setDados(prev =&gt; prev.filter((_, i) =&gt; i !== id));

};

return &lt;&gt;{children({ dados, carregando, erro, criar, deletar })}&lt;/&gt;;

}

// Uso

interface Post {

id: number;

titulo: string;

corpo: string;

}

const servicoPosts: Servico&lt;Post&gt; = {

obter: async () =&gt; {

const resp = await fetch(&#039;/api/posts&#039;);

return resp.json();

},

criar: async (post) =&gt; {

const resp = await fetch(&#039;/api/posts&#039;, {

method: &#039;POST&#039;,

body: JSON.stringify(post)

});

return resp.json();

},

deletar: async (id) =&gt; {

await fetch(/api/posts/${id}, { method: &#039;DELETE&#039; });

}

};

export default function App() {

return (

&lt;ProvedorServico&lt;Post&gt; servico={servicoPosts}&gt;

{({ dados, carregando, criar }) =&gt; (

&lt;div&gt;

{carregando ? &lt;p&gt;Carregando...&lt;/p&gt; : null}

{dados.map(post =&gt; (

&lt;article key={post.id}&gt;

&lt;h3&gt;{post.titulo}&lt;/h3&gt;

&lt;p&gt;{post.corpo}&lt;/p&gt;

&lt;/article&gt;

))}

&lt;/div&gt;

)}

&lt;/ProvedorServico&gt;

);

}</code></pre>

<h2>Conclusão</h2>

<p>Ao dominar componentes genéricos em React com TypeScript, você ganha três superpoderes principais. Primeiro, a <strong>reutilização genuína de código</strong>: um único componente funciona perfeitamente para dezenas de tipos diferentes sem comprometer a segurança de tipos. Segundo, a <strong>segurança em tempo de compilação</strong>: erros são capturados antes do código rodar em produção, reduzindo bugs significativamente. Terceiro, a <strong>experiência do desenvolvedor melhorada</strong>: IDEs fornecem autocompletar preciso e inferência de tipo inteligente porque o TypeScript entende exatamente qual tipo está sendo usado.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.typescriptlang.org/docs/handbook/2/generics.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Generics</a></li>

<li><a href="https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/basic_type_example/" target="_blank" rel="noopener noreferrer">React TypeScript Cheatsheet - Generics</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html" target="_blank" rel="noopener noreferrer">Advanced TypeScript - Conditional Types</a></li>

<li><a href="https://react.dev/reference/react/useContext" target="_blank" rel="noopener noreferrer">React Documentation - Context API</a></li>

<li><a href="https://www.leveluptutorials.com/tutorials/typescript-tutorial" target="_blank" rel="noopener noreferrer">Wes Bos - TypeScript Course</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Dominando APIs REST com Fastify e TypeScript: Schemas, Plugins e Types em Projetos Reais
Dominando APIs REST com Fastify e TypeScript: Schemas, Plugins e Types em Projetos Reais

Fundamentos de APIs REST com Fastify Fastify é um framework web moderno const...

Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis
Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis

O que são Arquivos .d.ts e Por Que Importam Os arquivos (TypeScript Declarati...

O que Todo Dev Deve Saber sobre Domain-Driven Design com TypeScript: Entidades e Value Objects Tipados
O que Todo Dev Deve Saber sobre Domain-Driven Design com TypeScript: Entidades e Value Objects Tipados

Fundamentos do Domain-Driven Design Domain-Driven Design (DDD) é uma metodolo...