Тип children в React и TypeScript: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что скрывается за пропсом children в React и TypeScript?

Если вы когда-нибудь писали компонент на React с использованием TypeScript, то наверняка сталкивались с вопросом: "Какой тип данных у пропса children?". Этот вопрос — один из самых популярных на Stack Overflow, и не зря. На первый взгляд кажется, что ответ простой: ReactNode. Но дьявол, как говорится, кроется в деталях.

В этой статье мы не просто дадим сухой ответ. Мы разберём все возможные типы для children, их отличия, лучшие практики и подводные камни. Вы узнаете, когда использовать ReactNode, когда ReactElement, а когда — более специфичные типы. Поехали!

Почему этот вопрос так популярен?

Проблема в том, что children — это особый пропс в React. Он может быть чем угодно:

  • Строкой: "Привет, мир!"
  • Числом: {42}
  • JSX-элементом: <div>...</div>
  • Массивом элементов: [<span/>, <span/>]
  • Функцией (render prop): {() => <div/>}
  • Даже null, undefined или boolean

Из-за такой гибкости новички часто пишут children: any, что сводит на нет всю пользу TypeScript. Давайте разберёмся с правильными подходами.

Основные типы для children

1. ReactNode — универсальный солдат

Это самый распространённый и правильный тип для children в 90% случаев. Он включает в себя всё, что React может отрендерить.

import { ReactNode } from 'react';

interface CardProps {
  title: string;
  children: ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">
        {children}
      </div>
    </div>
  );
}

// Использование:
<Card title="Моя карточка">
  <p>Этот текст будет отрендерен</p>
  {123} {/* Числа тоже работают */}
  {null} {/* И null */}
</Card>

Что входит в ReactNode? Фактически это объединение:

  • ReactElement — JSX-элементы
  • string и number — примитивы
  • boolean, null, undefined — игнорируются при рендере
  • ReactNode[] — массив из перечисленного
  • ReactPortal — порталы

2. ReactElement — строгий режим

Если вам нужно, чтобы children был только JSX-элементом (не строкой и не числом), используйте ReactElement.

import { ReactElement } from 'react';

interface StrictCardProps {
  children: ReactElement; // Только один JSX-элемент
}

function StrictCard({ children }: StrictCardProps) {
  return <div className="strict-card">{children}</div>;
}

// Работает:
<StrictCard><span>Только элемент</span></StrictCard>

// Ошибка TypeScript:
<StrictCard>Текст</StrictCard> // ❌ Строка не подходит

Важно: ReactElement требует ровно один дочерний элемент. Если передать два элемента без обёртки, TypeScript тоже выдаст ошибку.

3. JSX.Element — алиас для ReactElement

Тип JSX.Element — это просто ReactElement<any, any>. Он менее гибкий, чем ReactNode, и обычно не рекомендуется для children, если только вы не пишете библиотеку.

// По сути, это одно и то же:
type A = JSX.Element;    // ReactElement<any, any>
type B = ReactElement;   // ReactElement<Props, ElementType>

// Разница: ReactElement может быть более строгим с дженериками

Специфичные случаи типизации children

Типизация render prop (children как функция)

Иногда children — это не JSX, а функция, которая возвращает JSX. Это называется "render prop" или "children as function".

interface DataProviderProps<T> {
  url: string;
  children: (data: T | null, loading: boolean) => ReactNode;
}

function DataProvider<T>({ url, children }: DataProviderProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);

  // ... логика загрузки данных

  return <>{children(data, loading)}</>;
}

// Использование:
<DataProvider<User> url="/api/user">
  {(user, isLoading) => (
    isLoading ? <Spinner /> : <div>{user.name}</div>
  )}
</DataProvider>

Обратите внимание: в этом случае тип children — это функция, а не ReactNode.

Ограничение количества children

Иногда нужно, чтобы компонент принимал строго определённое количество дочерних элементов. Например, компонент SplitPane для двух панелей:

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

interface SplitPaneProps {
  children: [ReactElement, ReactElement]; // Кортеж из двух элементов
}

function SplitPane({ children }: SplitPaneProps) {
  const [left, right] = Children.toArray(children);
  
  return (
    <div className="split-pane">
      <div className="left-pane">{left}</div>
      <div className="right-pane">{right}</div>
    </div>
  );
}

// Правильное использование:
<SplitPane>
  <Sidebar />
  <MainContent />
</SplitPane>

// Ошибка:
<SplitPane>
  <Sidebar />
  <MainContent />
  <Footer /> // ❌ Третий элемент не допускается
</SplitPane>

Лучшие практики и рекомендации

1. Всегда используйте ReactNode для обычных компонентов

Если ваш компонент просто оборачивает дочерние элементы (как Card, Modal, Layout), используйте ReactNode. Это самый гибкий и безопасный выбор.

// ✅ Прави                

Блоги

Рекомендаций по книгам