Avatar ·

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

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

{title}

{children}
); } ``` Я хочу, чтобы этот компонент мог принимать: 1. Простой текст (строку) 2. Другой React-компонент 3. Несколько компонентов, обёрнутых во фрагмент 4. Любые JSX-элементы (div, span, img и т.д.) 5. Даже null или undefined (если children не передан) В JavaScript я просто писал `children: any`, но в TypeScript это считается плохой практикой. Я пробовал использовать `ReactNode`, `ReactElement`, `JSX.Element`, но запутался в различиях. Когда я пишу `children: React.ReactNode`, TypeScript жалуется, что я не импортировал React. Когда я использую `children: JSX.Element`, он не даёт передать строку. А `children: ReactChild` тоже не работает для массивов. Подскажите, пожалуйста, какой тип правильный и в чём разница между всеми этими типами? Буду очень благодарен за подробное объяснение! <<>>

Правильный тип для children — ReactNode

Самый универсальный и правильный тип для пропса children — это React.ReactNode. Он покрывает все возможные случаи использования. Вот как правильно его применить:

import React, { ReactNode } from 'react';

interface CardProps {
  title: string;
  children?: ReactNode; // ? означает, что children опционален
}

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

// Использование:
<Card title="Привет">
  Это строка
</Card>

<Card title="Компонент">
  <OtherComponent />
</Card>

<Card title="Несколько элементов">
  <span>Первый</span>
  <span>Второй</span>
</Card>

<Card title="Без children" /> {/* тоже работает */}

Разница между типами

Давайте разберём все типы, чтобы вы понимали, что и когда использовать:

  • ReactNode — самый широкий тип. Включает: ReactElement, строки, числа, boolean, null, undefined, массивы из ReactNode, фрагменты React. Это то, что может быть отрендерено React'ом.
  • ReactElement — только JSX-элементы (созданные через createElement или JSX). Не включает строки, числа, null. Используйте, если нужно гарантировать, что children — именно компонент.
  • JSX.Element — синоним ReactElement в большинстве случаев. Но это не React-специфичный тип, а глобальный тип для JSX.
  • ReactChild — устаревший тип, равный ReactElement | string | number. Не включает null, undefined и массивы.
  • ReactFragment — только фрагменты React. Крайне редко используется напрямую.

Почему ReactNode — лучший выбор?

В реальной разработке children может быть чем угодно: текстом (динамическим), другими компонентами, условно отрендеренными элементами (condition && <Component />), массивами из .map(). ReactNode покрывает все эти случаи.

Если вам нужно, чтобы children был обязательным (всегда передавался), уберите знак вопроса:

interface CardProps {
  title: string;
  children: ReactNode; // обязательно
}

Если вы используете React 18+, импорт React необязателен для JSX, но для типов его всё равно нужно импортировать:

import type { ReactNode } from 'react'; // type-only импорт (экономит бандл)

Дополнительный совет: children как функция (render props)

Иногда children может быть функцией (паттерн render props). Тогда тип будет другим:

interface DataProviderProps {
  children: (data: DataType) => ReactNode;
}

function DataProvider({ children }: DataProviderProps) {
  const data = { name: 'John' };
  return <>{children(data)}</>;
}

В этом случае children — функция, возвращающая ReactNode. Но это уже частный случай, для обычного использования достаточно ReactNode.

<<>>

Альтернативный подход: использование React.PropsWithChildren

В TypeScript есть встроенная утилита PropsWithChildren<P>, которая автоматически добавляет тип children?: ReactNode к вашим пропсам. Это удобно, когда нужно избежать ручного объявления:

import React, { PropsWithChildren } from 'react';

interface CardProps {
  title: string;
}

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

Этот подход особенно полезен, когда у вас много компонентов-обёрток и вы не хотите каждый раз писать children?: ReactNode. PropsWithChildren делает код чище и следует принципу DRY.

Обратите внимание: PropsWithChildren делает children опциональным (со знаком ?). Если children обязателен, лучше использовать явное объявление с ReactNode без опциональности.

Ещё один вариант — React.FC<CardProps> (устаревший в React 18+, но всё ещё распространённый). Он автоматически добавляет children в пропсы:

const Card: React.FC<CardProps> = ({ title, children }) => {
  // children уже типизирован как ReactNode
};

Однако в современном React (18+) рекомендуется использовать обычные функции вместо React.FC, так как это даёт больше гибкости и меньше магии.