Avatar ·

Когда использовать JSX.Element, ReactNode и ReactElement в TypeScript с React?

Привет! Я начал изучать TypeScript вместе с React и постоянно путаюсь в типах для описания того, что может быть возвращено из компонента или передано как children. В интернете часто встречаю три типа: `JSX.Element`, `ReactNode` и `ReactElement`. Но когда какой использовать — для меня загадка. Например, я пишу компонент обёртку: ```typescript interface WrapperProps { children: ??? // Что тут писать? } ``` Или когда хочу явно указать тип возвращаемого значения функции-компонента: ```typescript const MyComponent = (): ??? => { return
Hello
; }; ``` Я пробовал использовать `ReactNode`, но тогда TypeScript ругается, когда я пытаюсь передать что-то вроде массива компонентов. Потом попробовал `JSX.Element` — вроде работает, но не уверен, что это правильно. А про `ReactElement` вообще не понимаю, зачем он нужен. Объясните, пожалуйста, на пальцах: в чём разница между этими тремя типами и в каких ситуациях какой из них применять? Буду очень благодарен за примеры кода с пояснениями. <<>>

Краткая шпаргалка: что есть что

Давайте сразу разложим по полочкам, а потом разберём на примерах.

  • ReactElement — это объект, который описывает один React-элемент (обычно создаётся через createElement или JSX). Это самый базовый тип. Он всегда имеет type, props и key.
  • JSX.Element — это просто синоним (alias) для ReactElement с конкретными пропсами any. По сути, это то же самое, но с более широким типом пропсов.
  • ReactNode — это супер-тип, который включает всё, что React может отрендерить: ReactElement, строки, числа, булевы значения, null, undefined, фрагменты, массивы из всего перечисленного.

Когда что использовать?

1. Типизация возвращаемого значения компонента

Если вы пишете функциональный компонент, обычно тип возвращаемого значения можно не указывать — TypeScript сам выведет его из JSX. Но если очень хочется:

// ❌ Неправильно — слишком узко
const BadComponent = (): ReactElement => {
  return <div>Hi</div>; // Работает, но не принимает строки
};

// ✅ Правильно — ReactNode (но чаще вообще не нужно)
const GoodComponent = (): ReactNode => {
  return <div>Hi</div>; // Работает
};

// Лучший вариант — вообще не указывать тип
const BestComponent = () => {
  return <div>Hi</div>; // TypeScript сам выведет JSX.Element
};

2. Типизация children

Это самый частый случай. Вот правильные варианты:

// ✅ Для большинства случаев — ReactNode
interface WrapperProps {
  children: ReactNode;
}

// ✅ Если хотите строго один элемент — ReactElement
interface StrictWrapperProps {
  children: ReactElement; // Только один элемент, не строка, не массив
}

// ✅ Если children должен быть только React-компонентом — JSX.Element
interface ComponentWrapperProps {
  children: JSX.Element; // По сути то же, что ReactElement
}

// Пример использования:
const Card = ({ children }: { children: ReactNode }) => {
  return <div className="card">{children}</div>;
};

// Можно передать всё что угодно:
<Card>
  Hello World           {/* строка — ок */}
  {42}                  {/* число — ок */}
  {null}                {/* null — ок */}
  <span>Hi</span>      {/* элемент — ок */}
  {[<span>A</span>, <span>B</span>]}  {/* массив — ок */}
</Card>

3. Типизация пропсов, которые принимают один элемент

Если ваш компонент принимает иконку или какой-то обязательный элемент:

interface ButtonProps {
  icon: ReactElement; // Обязательно один элемент
  label: string;
}

const Button = ({ icon, label }: ButtonProps) => {
  return (
    <button>
      {icon}
      <span>{label}</span>
    </button>
  );
};

// Использование:
<Button icon={<StarIcon />} label="Избранное" />
// ❌ Нельзя передать строку: icon="⭐" — ошибка типов

Практические рекомендации

  1. Для children всегда используйте ReactNode — это самый гибкий и правильный вариант.
  2. Для возвращаемого типа компонента не указывайте тип вообще — пусть TypeScript выводит сам.
  3. Для строгих требований (например, иконка должна быть именно React-элементом) используйте ReactElement или JSX.Element.
  4. ReactElement и JSX.Element — взаимозаменяемы в 99% случаев, но ReactElement предпочтительнее, так как это часть React API.

Наглядная иерархия типов

Если представить типы в виде вложенных кругов:

  • Самый внешний круг — ReactNode: строки, числа, null, undefined, boolean, ReactElement, массивы из всего этого.
  • Внутри — ReactElement: только объекты, созданные через createElement/JSX.
  • Самый внутренний — JSX.Element: практически то же самое, что ReactElement, но с типом пропсов any.

Надеюсь, теперь стало понятнее! Главное правило: если сомневаетесь — используйте ReactNode для children и не указывайте тип для возврата компонента.

<<>>

Дополнение: когда ReactNode может быть опасен

Хотя ReactNode — самый универсальный тип, есть ситуации, где его лучше избегать.

Проблема с условным рендерингом

Если вы используете ReactNode для children и внутри компонента применяете React.Children или Children.toArray, TypeScript может не дать вам нормально работать с элементами, потому что ReactNode включает string | number | boolean | null | undefined.

// ❌ Проблема: не можем вызвать методы ReactElement
const BadList = ({ children }: { children: ReactNode }) => {
  // Ошибка! children может быть строкой
  return <div>{React.Children.map(children, child => child.type)}</div>;
};

// ✅ Решение: используем ReactElement
const GoodList = ({ children }: { children: ReactElement | ReactElement[] }) => {
  return <div>{React.Children.map(children, child => child.type)}</div>;
};

Когда использовать JSX.Element вместо ReactElement?

Если вы работаете с библиотекой, которая ожидает именно JSX (например, некоторые CSS-in-JS решения), лучше использовать JSX.Element. Но в обычной разработке разницы нет.

Совет для новичков

Начните с ReactNode для children. Если TypeScript начинает ругаться или вы чувствуете, что нужна более строгая типизация — переходите на ReactElement. И никогда не указывайте тип возврата для компонента — это лишнее.