Какой тип данных у пропса children в React и TypeScript?
{title}
Основной тип: ReactNode
Самый универсальный и рекомендуемый тип для пропса children — это ReactNode. Он покрывает практически все возможные варианты: строки, числа, JSX-элементы, фрагменты, порталы, null, undefined и даже массивы из перечисленного.
В вашем случае правильный код будет выглядеть так:
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>
);
}
Разница между типами
Давайте разберём, чем отличаются похожие типы, чтобы вы понимали, когда что использовать:
- ReactNode — самый широкий тип. Включает всё: строки, числа, булевы значения,
null,undefined, ReactElement, ReactFragment, ReactPortal и массивы из них. Используйте его по умолчанию. - ReactElement — более узкий тип. Это объект, представляющий JSX-элемент (например,
<div/>). Он НЕ включает строки, числа или массивы. Используйте, если хотите гарантировать, что children — это именно JSX-компонент. - ReactChild — устаревший тип (deprecated с React 18). Это объединение
ReactElement | string | number. Не используйте его в новом коде. - JSX.Element — по сути то же самое, что
ReactElement, но из пространства имён JSX. Лучше использоватьReactElementдля ясности.
Строгие варианты children
Иногда вам может понадобиться ограничить children:
- Только один элемент: используйте
ReactElement(или конкретный тип компонента). - Только строка:
children: string. - Только функция (render prop):
children: (value: T) => ReactNode. - Ничего (пустой children): просто не указывайте children в интерфейсе.
Пример для строго одного дочернего элемента:
interface ButtonProps {
children: ReactElement; // Только один JSX-элемент
}
function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
// ✅ Работает: <Button><span>Click</span></Button>
// ❌ Ошибка: <Button>Click</Button> (строка не подходит)
Современный best practice
На 2024 год стандарт такой:
- Для обычных компонентов-обёрток используйте ReactNode. Это безопасно и гибко.
- Импортируйте тип из React:
import type { ReactNode } from 'react'; - Избегайте
any— это убивает всю пользу TypeScript. - Если компонент должен принимать строго определённые дочерние элементы (например, только
<MenuItem />внутри<Menu />), используйте дженерики или специальные типы.
Также помните, что children — это просто пропс, и его можно типизировать как любой другой пропс. React не накладывает на него никаких магических ограничений (кроме того, что JSX-транспилятор передаёт содержимое между тегами именно в этот пропс).
Альтернативный подход: использование дженериков
Если вы пишете компонент, который должен работать с дочерними элементами определённого типа, можно использовать дженерики. Это даёт более строгую типизацию и автодополнение в IDE.
Пример — компонент списка, который принимает только элементы ListItem:
import { ReactElement } from 'react';
interface ListProps<T extends ReactElement> {
items: T[];
children: T; // или ReactElement<ListItemProps>
}
function List<T extends ReactElement>({ items, children }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
{children}
</ul>
);
}
Когда это полезно?
Такой подход пригодится в библиотеках компонентов (например, Material-UI или Ant Design), где нужно гарантировать, что внутри <Select> будут только <Option>, а внутри <Table> — только <Column>. Для обычных проектов ReactNode более чем достаточно.
Также обратите внимание, что в React 18 появился тип ReactNode с улучшенной поддержкой undefined и boolean — теперь они корректно исключаются из рендера, но остаются валидными для TypeScript.