Pular para o conteúdo
TypeScript

Tipar Children no React com TypeScript

ReactNode, ReactElement, PropsWithChildren... qual usar? Descubra a diferenca e quando aplicar cada tipo de children no React com TypeScript.

Por que isso é importante

Tipar Children no React com TypeScript. ReactNode, ReactElement, PropsWithChildren... qual usar? Descubra a diferenca e quando aplicar cada tipo de children no React com TypeScript.

A diferenca entre ReactNode, ReactElement e JSX.Element

Galera, essa e a duvida mais comum. O React tem tres tipos pra representar "coisas que podem ser renderizadas" e cada um tem um escopo diferente. Vamos direto ao ponto:

ReactNode e o tipo mais amplo. Aceita elementos JSX, strings, numeros, booleanos, null, undefined e arrays de tudo isso. Na maioria dos casos, e o que voce quer pra children. ReactElement e mais restrito: so aceita elementos JSX criados com React.createElement. Nao aceita string, numero ou null. JSX.Element e basicamente igual ao ReactElement mas com props tipados como any.

import { ReactNode, ReactElement } from 'react';

// ReactNode aceita TUDO que pode ser renderizado
type ReactNode =
  | ReactElement
  | string
  | number
  | boolean
  | null
  | undefined
  | Iterable<ReactNode>;

// ReactElement: so elementos JSX
interface ReactElement {
  type: string | ComponentType;
  props: object;
  key: string | null;
}

// Comparacao pratica
const a: ReactNode = 'texto';        // OK
const b: ReactNode = 42;             // OK
const c: ReactNode = null;           // OK
const d: ReactNode = <div />;        // OK

const e: ReactElement = 'texto';     // ERRO!
const f: ReactElement = 42;          // ERRO!
const g: ReactElement = <div />;     // OK

Passo a passo: tipando children corretamente

  1. Use ReactNode como tipo padrao para children — cobre 95% dos casos
  2. Use ReactElement quando o componente precisa receber so JSX, nunca texto puro
  3. Use PropsWithChildren quando quer adicionar children a props existentes rapido
  4. Para render props, tipe children como funcao que recebe parametros e retorna ReactNode
  5. Marque children como obrigatorio removendo o ? quando o componente nao faz sentido vazio
  6. Evite JSX.Element — prefira sempre ReactNode ou ReactElement

Exemplos praticos de tipagem de children

Componente wrapper com ReactNode

O caso mais comum: um componente que envolve outros com algum estilo ou logica. Da pra receber qualquer conteudo renderizavel.

import { ReactNode } from 'react';

// Jeito explicito: declarando children na interface
interface CardProps {
  title: string;
  children: ReactNode;
  variant?: 'default' | 'highlighted';
}

function Card({ title, children, variant = 'default' }: CardProps) {
  return (
    <div className={`card card--${variant}`}>
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Todos esses usos sao validos:
<Card title="Info">Texto simples</Card>
<Card title="Info">{42}</Card>
<Card title="Info"><p>Elemento JSX</p></Card>
<Card title="Info">
  <p>Multiplos</p>
  <p>Elementos</p>
</Card>

PropsWithChildren: o atalho do React

Se voce ja tem uma interface de props e quer adicionar children sem digitar de novo, PropsWithChildren resolve. Ele adiciona children?: ReactNode automaticamente.

import { PropsWithChildren } from 'react';

// Sem PropsWithChildren
interface LayoutProps {
  sidebar: ReactNode;
  children: ReactNode;
}

// Com PropsWithChildren (resultado identico)
type LayoutProps2 = PropsWithChildren<{
  sidebar: ReactNode;
}>;

function Layout({ sidebar, children }: LayoutProps2) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

// PropsWithChildren sem props extras
type WrapperProps = PropsWithChildren;

function Wrapper({ children }: WrapperProps) {
  return <div className="wrapper">{children}</div>;
}

ReactElement: quando precisa restringir

Em alguns casos voce quer que o componente receba so elementos JSX, nao texto solto. Um exemplo: componente de Tabs que precisa iterar sobre os filhos e ler suas props.

import { ReactElement, Children, isValidElement, cloneElement } from 'react';

interface TabProps {
  label: string;
  children: ReactNode;
}

interface TabsProps {
  children: ReactElement<TabProps> | ReactElement<TabProps>[];
  activeIndex: number;
}

function Tabs({ children, activeIndex }: TabsProps) {
  const tabs = Children.toArray(children)
    .filter(isValidElement) as ReactElement<TabProps>[];

  return (
    <div>
      <nav>
        {tabs.map((tab, i) => (
          <button key={i} className={i === activeIndex ? 'active' : ''}>
            {tab.props.label}
          </button>
        ))}
      </nav>
      <div>{tabs[activeIndex]}</div>
    </div>
  );
}

// Uso correto
<Tabs activeIndex={0}>
  <Tab label="Geral">Conteudo geral</Tab>
  <Tab label="Config">Configuracoes</Tab>
</Tabs>

// Erro: texto nao e ReactElement
<Tabs activeIndex={0}>
  Texto solto  {/* TypeScript Error! */}
</Tabs>

Render props: children como funcao

Render props e um padrao poderoso onde children e uma funcao. O TypeScript garante que o consumidor passe uma funcao com a assinatura certa e use os parametros corretamente.

import { useState, ReactNode } from 'react';

// Children como funcao tipada
interface ToggleProps {
  children: (props: {
    isOn: boolean;
    toggle: () => void;
  }) => ReactNode;
}

function Toggle({ children }: ToggleProps) {
  const [isOn, setIsOn] = useState(false);
  const toggle = () => setIsOn(prev => !prev);

  return <>{children({ isOn, toggle })}</>;
}

// Uso: TypeScript valida os parametros da funcao
<Toggle>
  {({ isOn, toggle }) => (
    <button
      {isOn ? 'Ligado' : 'Desligado'}
    </button>
  )}
</Toggle>

// Render prop generica com dados
interface FetchRenderProps<T> {
  url: string;
  children: (props: {
    data: T | null;
    loading: boolean;
    error: string | null;
  }) => ReactNode;
}

function Fetch<T>({ url, children }: FetchRenderProps<T>) {
  // ... logica de fetch
  return <>{children({ data: null, loading: true, error: null })}</>;
}

Erros comuns ao tipar children

Cuidado com essas armadilhas

Usar JSX.Element como tipo de children e um erro sutil: ele nao aceita strings nem numeros, entao Ola da erro de tipo. Outro problema classico: esquecer que PropsWithChildren faz children opcional por padrao — se o componente precisa de children, declare explicitamente sem o ? na interface.

// ERRADO: JSX.Element rejeita texto
interface BadProps {
  children: JSX.Element;
}
function Bad({ children }: BadProps) { return <div>{children}</div>; }
<Bad>Texto</Bad> // ERRO: string nao e JSX.Element

// CERTO: ReactNode aceita tudo
interface GoodProps {
  children: ReactNode;
}

// ERRADO: PropsWithChildren faz children opcional
type OptionalKids = PropsWithChildren<{ title: string }>;
// children?: ReactNode (pode ser undefined!)

// CERTO: declare children obrigatorio
interface RequiredKids {
  title: string;
  children: ReactNode; // sem ?, sempre obrigatorio
}

// ERRADO: tipar children como string[]
interface ListProps {
  children: string[]; // Muito restritivo
}

// CERTO: aceitar ReactNode e tratar no componente
interface ListProps2 {
  children: ReactNode;
}

Checklist: children tipado corretamente

Checklist Final

  • ReactNode usado como tipo padrao para children na maioria dos componentes
  • ReactElement usado apenas quando precisa restringir a elementos JSX
  • PropsWithChildren usado como atalho em interfaces simples
  • Children obrigatorio declarado sem ? quando componente precisa de conteudo
  • Render props tipam children como funcao com parametros definidos
  • JSX.Element evitado como tipo de children
  • Componentes de Tabs/Accordion usam ReactElement com generico de props
  • Nenhum 'any' no tipo de children

Leve sua tipagem React pro proximo nivel

Projeto completo com TypeScript

Saber tipar children direito e o que torna seus componentes reutilizaveis de verdade. No CrazyStack voce constroi componentes como esses dentro de um projeto real completo, usando TypeScript, React e Node.js. Do layout ate a API, tudo com tipagem profissional.