Avatar ·

Какой тип данных у пропса children в React и TypeScript?

Всем привет! Я начал недавно изучать TypeScript вместе с React и столкнулся с непонятной ситуацией. Я создаю компонент-обёртку, который должен принимать дочерние элементы. В обычном JavaScript я просто писал `props.children` и всё работало. Но в TypeScript мне нужно указывать типы, и я не понимаю, какой тип указать для `children`. Вот пример моего кода: ```tsx interface CardProps { title: string; children: ???; // Что писать здесь? } function Card({ title, children }: CardProps) { return (

{title}

{children}
); } ``` Я пробовал писать `children: any`, но это не очень хорошо, теряется типизация. Пробовал `children: ReactNode` — вроде работает, но не уверен, правильно ли это. А ещё я видел, что кто-то использует `ReactChild` или `ReactElement`. В чём разница? И что если я хочу, чтобы компонент принимал только один дочерний элемент? Или наоборот — только массив? Помогите разобраться, пожалуйста. Хочется понять, какой тип использовать в разных ситуациях и какой считается best practice на 2024 год. <<>>

Основной тип: 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.