Что скрывается за пропсом 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. Это самый гибкий и безопасный выбор.
// ✅ Прави