Pular para o conteúdo
TypeScript

Componente Genérico React com TypeScript

Componentes genericos sao o superpoder do TypeScript no React. Aprenda a criar listas, selects, formularios e tables que funcionam com qualquer tipo de dado.

Por que isso é importante

Componente Genérico React com TypeScript. Componentes genericos sao o superpoder do TypeScript no React. Aprenda a criar listas, selects, formularios e tables que funcionam com qualquer tipo de dado.

O que e um componente generico no React

Um componente generico e uma funcao React que recebe um tipo como parametro alem das props normais. Em vez de tipar os dados como 'any' ou criar N versoes do mesmo componente, voce usa um generico T que o TypeScript resolve automaticamente baseado nos dados que voce passa.

Pensa assim: quando voce faz useState(''), o useState e generico e voce passa string como tipo. Componentes genericos funcionam do mesmo jeito, so que voce define o generico na hora de usar o componente.

// Componente NAO generico (restrito a um tipo)
interface UserListProps {
  items: User[];
  renderItem: (item: User) => ReactNode;
}

interface ProductListProps {
  items: Product[];
  renderItem: (item: Product) => ReactNode;
}
// Codigo duplicado! Mesma estrutura, tipos diferentes.

// Componente generico (funciona com qualquer tipo)
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => ReactNode;
}
// Um unico componente pra todos os casos.

Passo a passo: criando componente generico

  1. Defina a interface de props com o generico nos dados que variam
  2. Crie a funcao do componente com a sintaxe function Component(props: Props)
  3. Use constraints (extends) quando o generico precisa ter campos obrigatorios
  4. Passe o tipo ao usar o componente ou deixe o TypeScript inferir pelos dados
  5. Para forwardRef com generics, crie um wrapper com tipagem manual
  6. Adicione defaults com = nos generics quando fizer sentido

Exemplos praticos de componentes genericos

Lista generica reutilizavel

O exemplo classico: uma lista que renderiza qualquer tipo de item. Voce passa os dados e uma funcao de render, e o TypeScript garante que tudo bate.

import { ReactNode } from 'react';

// Props genericas com constraint
interface ListProps<T extends { id: string | number }> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
  emptyMessage?: string;
  loading?: boolean;
  className?: string;
}

// Componente generico
function List<T extends { id: string | number }>({
  items,
  renderItem,
  emptyMessage = 'Nenhum item encontrado',
  loading = false,
  className,
}: ListProps<T>) {
  if (loading) return <div>Carregando...</div>;

  if (items.length === 0) {
    return <p className="text-gray-500">{emptyMessage}</p>;
  }

  return (
    <ul className={className}>
      {items.map((item, index) => (
        <li key={item.id}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// Uso com User — TypeScript infere T = User
interface User {
  id: string;
  name: string;
  email: string;
}

const users: User[] = [
  { id: '1', name: 'Ana', email: 'ana@mail.com' },
  { id: '2', name: 'Carlos', email: 'carlos@mail.com' },
];

<List
  items={users}
  renderItem={(user) => (
    <div>
      <strong>{user.name}</strong> {/* autocomplete funciona! */}
      <span>{user.email}</span>
    </div>
  )}
/>

// Uso com Product — mesmo componente, tipo diferente
interface Product {
  id: number;
  name: string;
  price: number;
}

<List
  items={products}
  renderItem={(product) => (
    <div>
      {product.name} - R${product.price} {/* tipado! */}
    </div>
  )}
/>

Select generico

Outro componente que todo projeto precisa: um select que funciona com qualquer tipo de opcao. O generico garante que o onChange retorna o tipo certo.

import { ReactNode } from 'react';

interface SelectProps<T> {
  options: T[];
  value: T | null;
  onChange: (selected: T) => void;
  getLabel: (option: T) => string;
  getValue: (option: T) => string;
  placeholder?: string;
}

function Select<T>({
  options,
  value,
  onChange,
  getLabel,
  getValue,
  placeholder = 'Selecione...',
}: SelectProps<T>) {
  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const selected = options.find(
      (opt) => getValue(opt) === e.target.value
    );
    if (selected) onChange(selected);
  };

  return (
    <select
      value={value ? getValue(value) : ''}
    >
      <option value="">{placeholder}</option>
      {options.map((opt) => (
        <option key={getValue(opt)} value={getValue(opt)}>
          {getLabel(opt)}
        </option>
      ))}
    </select>
  );
}

// Uso: TypeScript sabe que selected e do tipo Country
interface Country {
  code: string;
  name: string;
  population: number;
}

const [country, setCountry] = useState<Country | null>(null);

<Select
  options={countries}
  value={country}          // (selected: Country) => void
  getLabel={(c) => c.name}       // TypeScript sabe que c e Country
  getValue={(c) => c.code}
/>

Table generica com colunas tipadas

Uma table generica e mais sofisticada. Voce define as colunas com accessor tipado, e o TypeScript garante que a coluna referencia um campo real do tipo de dado.

import { ReactNode } from 'react';

// Coluna tipada com keyof
interface Column<T> {
  header: string;
  accessor: keyof T;
  render?: (value: T[keyof T], row: T) => ReactNode;
}

interface TableProps<T extends { id: string | number }> {
  data: T[];
  columns: Column<T>[];
  onRowClick?: (row: T) => void;
}

function Table<T extends { id: string | number }>({
  data,
  columns,
  onRowClick,
}: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            <th key={String(col.accessor)}>{col.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row) => (
          <tr
            key={row.id} => onRowClick?.(row)}
            style={{ cursor: onRowClick ? 'pointer' : 'default' }}
          >
            {columns.map((col) => (
              <td key={String(col.accessor)}>
                {col.render
                  ? col.render(row[col.accessor], row)
                  : String(row[col.accessor])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Uso: colunas tipadas com base no tipo Employee
interface Employee {
  id: number;
  name: string;
  role: string;
  salary: number;
}

const columns: Column<Employee>[] = [
  { header: 'Nome', accessor: 'name' },
  { header: 'Cargo', accessor: 'role' },
  {
    header: 'Salario',
    accessor: 'salary',
    render: (value) => `R$ ${Number(value).toFixed(2)}`,
  },
  // { header: 'Teste', accessor: 'inexistente' } // ERRO!
];

<Table
  data={employees}
  columns={columns} => console.log(emp.name)}
/>

forwardRef com generics

Esse e o caso mais avancado. O forwardRef do React nao suporta generics diretamente, entao voce precisa de um workaround com type assertion. Olha como fica:

import { forwardRef, Ref, ReactNode } from 'react';

// Props genericas do input
interface GenericInputProps<T> {
  value: T;
  onChange: (value: T) => void;
  parse: (raw: string) => T;
  format: (value: T) => string;
  label: string;
}

// Workaround: componente interno generico
function GenericInputInner<T>(
  {
    value,
    onChange,
    parse,
    format,
    label,
  }: GenericInputProps<T>,
  ref: Ref<HTMLInputElement>
) {
  return (
    <label>
      {label}
      <input
        ref={ref}
        value={format(value)} => onChange(parse(e.target.value))}
      />
    </label>
  );
}

// Type assertion pra manter o generico
export const GenericInput = forwardRef(GenericInputInner) as <T>(
  props: GenericInputProps<T> & { ref?: Ref<HTMLInputElement> }
) => ReactNode;

// Uso com numero
<GenericInput
  value={42} => console.log(n)} // n e number
  parse={Number}
  format={String}
  label="Quantidade"
/>

// Uso com Date
<GenericInput
  value={new Date()} => console.log(d)} // d e Date
  parse={(s) => new Date(s)}
  format={(d) => d.toISOString().split('T')[0]}
  label="Data"
/>

Erros comuns com componentes genericos

Armadilhas que travam a galera

O erro mais frequente e esquecer do constraint (extends) e tentar acessar propriedades do generico. Se T nao tem constraint, o TypeScript trata como unknown e voce nao consegue acessar nada. Outro problema: em arquivos .tsx, a sintaxe de arrow functions confunde o parser com JSX. Use com virgula ou function declarations.

// ERRADO: acessar .id sem constraint
function List<T>({ items }: { items: T[] }) {
  return items.map(item => (
    <li key={item.id}>{/* ERRO: Property 'id' does not exist on type 'T' */}</li>
  ));
}

// CERTO: constraint garante que T tem id
function List<T extends { id: string | number }>({ items }: { items: T[] }) {
  return items.map(item => (
    <li key={item.id}>...</li> // OK!
  ));
}

// ERRADO em .tsx: parser confunde com JSX tag
const MyComponent = <T>(props: Props<T>) => { ... };
// Erro de syntax!

// CERTO: virgula desambigua o generico
const MyComponent = <T,>(props: Props<T>) => { ... };

// OU: use function declaration (mais limpo)
function MyComponent<T>(props: Props<T>) { ... }

// ERRADO: generico desnecessario
function Badge<T>({ text }: { text: string }) {
  // T nunca e usado! Remove o generico.
  return <span>{text}</span>;
}

Checklist: componente generico profissional

Checklist Final

  • Generico T definido com constraints relevantes (extends { id: ... })
  • Function declarations usadas ao inves de arrow functions em .tsx
  • Cada generico e realmente usado nas props — nenhum T solto
  • keyof T usado para accessor de colunas e campos dinamicos
  • forwardRef com type assertion quando precisa de ref + generics
  • Inferencia de tipo testada — TypeScript resolve T sem passar explicitamente
  • Componente funciona com pelo menos 3 tipos diferentes de dados
  • Nenhum 'any' usado como escape de tipagem generica

Construa componentes profissionais no projeto real

Projeto completo com TypeScript

Componentes genericos sao o que separa projetos amadores de projetos profissionais em React. No CrazyStack voce constroi um sistema completo com tables, selects, formularios e listas genericas — tudo tipado e pronto pra producao com TypeScript, React e Node.js.