JSX.Element vs ReactNode vs ReactElement: TS2322 Fix
technology

JSX.Element vs ReactNode vs ReactElement: TS2322 Fix

Fix TS2322 by understanding when ReactNode, JSX.Element, and ReactElement apply in React + TypeScript component typing.

2026-06-20
9 min read
JSX.Element vs ReactNode vs ReactElement: TS2322 Fix

TL;DR#

If you’re seeing TS2322: Type 'null' is not assignable to type 'JSX.Element' or Type 'string' is not assignable to type 'JSX.Element', the root cause is usually using JSX.Element where ReactNode is required — especially in component return types or props that accept flexible content like children or render props.

Fix it by replacing JSX.Element with ReactNode for return types and props that need to support null, strings, numbers, or arrays — and reserve JSX.Element for strict JSX-only contexts like render props where you know only JSX will be passed.

If that doesn’t work, check whether you’re accidentally using ReactElement (a runtime object type) where ReactNode (a renderable value type) is expected.

The error#

You’ll see errors like these in your editor or terminal:

plaintext
TS2322: Type 'null' is not assignable to type 'JSX.Element'.
TS2322: Type 'string' is not assignable to type 'JSX.Element'.
TS2322: Type 'Element' is not assignable to type 'JSX.Element'.
TS2322: Type 'ReactElement<...>' is not assignable to type 'JSX.Element'.

It happens when you type a component’s return value, a prop (like children, render, or fallback), or a render prop function as JSX.Element — but then return or pass something like null, a plain string, a number, or an array of elements.

Why JSX.Element is too narrow#

TypeScript’s React types are layered, and the confusion comes from conflating what JSX compiles to with what React can render. Let’s untangle them.

JSX.Element is the type that JSX expressions (like <div />) are inferred to have before React transforms them. In older React versions, JSX.Element was defined by @types/react as ReactElement<any, any>. But in modern React (18+), JSX.Element is intentionally kept narrow: it only covers valid JSX syntax, not arbitrary renderable values.

ReactElement, on the other hand, is the actual runtime object created by React.createElement(). It’s a concrete type with properties like type, props, and key. You rarely need to use it directly — and using it as a prop type is almost always wrong.

ReactNode is the union type React uses internally to define what can be rendered: ReactNode = ReactElement | string | number | boolean | null | undefined | ReactNode[] | Portal | Fragment. It’s the correct type for component return values and most props that accept content.

tsx
// src/components/Card.tsx
import React from 'react';
 
// ❌ Wrong: JSX.Element excludes null, strings, numbers, arrays
function Card({ title, content }: { title: string; content: string }) {
  if (!content) {
    return null; // ← TS2322: Type 'null' is not assignable to type 'JSX.Element'
  }
 
  return (
    <div>
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
}
 
// ❌ Also wrong: JSX.Element doesn’t accept arrays of elements
function List({ items }: { items: string[] }) {
  return items.map((item) => <li key={item}>{item}</li>); // ← TS2322
}

TypeScript rejects null and arrays here because JSX.Element is not a superset of ReactNode. It’s a subset — and only covers single, non-null JSX elements.

The switch#

Replace JSX.Element with ReactNode in return types and props that need flexibility. Reserve JSX.Element only for cases where you explicitly want to forbid non-JSX values (like render props that must return JSX).

tsx
// src/components/Card.tsx
import React from 'react';
 
// ✅ Correct: ReactNode accepts null, strings, numbers, arrays
function Card({ title, content }: { title: string; content: string }) {
  if (!content) {
    return null;
  }
 
  return (
    <div>
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
}
 
// ✅ Also correct: ReactNode handles arrays naturally
function List({ items }: { items: string[] }) {
  return items.map((item) => <li key={item}>{item}</li>);
}
 
// ✅ For render props: use JSX.Element *only* if you want JSX-only
function Modal({
  renderHeader,
}: {
  renderHeader: () => JSX.Element; // ← strict JSX-only
}) {
  return <div>{renderHeader()}</div>;
}
 
// ✅ For flexible content (like children): use ReactNode
function Box({ children }: { children: React.ReactNode }) {
  return <div className="box">{children}</div>;
}

That single change addresses the cause because ReactNode is the official type React uses to define what can be rendered — it’s designed to match runtime behavior, not just JSX syntax.

Step by step#

  1. Open the file where you see the TS2322 error.
  2. Find the component or function with the incorrect type (e.g., (): JSX.Element or { render: JSX.Element }).
  3. Replace JSX.Element with ReactNode in return types and props like children, fallback, empty, or render.
  4. Save and run tsc --noEmit to confirm no errors.

Component props#

Typing component props is the most common place developers reach for JSX.Element and immediately hit TS2322. A prop describes a slot the consumer fills — and that slot may legitimately receive null, a string, a number, an array, or a fragment, not just a single JSX element.

The pattern to internalize: when a prop represents renderable content, type it as ReactNode. When it represents a function that must produce JSX, type it as JSX.Element.

tsx
// Common component props patterns
 
// ✅ Content slot: ReactNode
type ContainerProps = {
  children: React.ReactNode;
};
 
// ✅ Optional sections: ReactNode (not JSX.Element)
type CardProps = {
  header?: React.ReactNode;
  footer?: React.ReactNode;
  body: React.ReactNode;
};
 
// ✅ Render prop producing JSX: JSX.Element enforces a JSX-only return
type TableProps<T> = {
  items: T[];
  renderRow: (item: T) => JSX.Element;
  renderEmpty: () => JSX.Element;
};
 
// ✅ Render prop that may return null/string: ReactNode
type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  renderEmpty?: () => React.ReactNode;
};
 
// ❌ Common mistake: JSX.Element on a content prop
type BadProps = {
  icon: JSX.Element; // ← rejects strings like icon="save", and null
};

Rules of thumb for component props:

  • children and any "slot" prop (header, footer, body, fallback, empty, overlay, tooltip): use ReactNode. Optional slots should be ReactNode | undefined (or just ReactNode — it already includes undefined).
  • Render props where the consumer must return JSX (renderRow, renderHeader, renderCell): use JSX.Element to enforce a strict JSX return.
  • Render props that may return a string, number, or null (renderLabel, renderEmpty): use ReactNode.
  • Avoid ReactElement as a prop type. It’s a runtime object shape ({ type, props, key }) — consumers don’t think in those terms, and it’s not assignable from arbitrary JSX in modern React.

If you’re publishing a component library, defaulting to ReactNode for every content prop is the most forgiving and version-stable choice. It accepts everything a React render can produce, and you won’t break consumers when they pass a conditional null or a fragment.

Function return types#

The second hot spot is function return types — both for React component functions and for helper functions that build JSX. The error pattern is always the same: a function is annotated as (): JSX.Element (or : ReactElement<...>), but the body returns null, a primitive, or an array.

The correct function return type depends on what the function is allowed to produce:

tsx
import React, { ReactNode, ReactElement } from 'react';
 
// ✅ React component: ReactNode covers every legal render output
function Greeting({ name }: { name?: string }): React.ReactNode {
  if (!name) return null;          // valid — ReactNode includes null
  if (name === 'admin') return `Welcome, ${name}`; // valid — string
  return <h1>Hello, {name}</h1>;   // valid — JSX
}
 
// ❌ Component returning null with JSX.Element annotation
function Greeting({ name }: { name?: string }): JSX.Element {
  if (!name) return null; // ← TS2322
  return <h1>Hello, {name}</h1>;
}
 
// ✅ Helper that always returns a single element: JSX.Element is fine
function Spinner({ size = 16 }: { size?: number }): JSX.Element {
  return <div className="spinner" style={{}} />;
}
 
// ❌ Helper that may return null: use ReactNode
function MaybeSpinner({ loading }: { loading: boolean }): ReactNode {
  if (!loading) return null;
  return <Spinner />;
}
 
// ❌ Don't use ReactElement as a return type — it's an object shape, not a renderable value
function Broken(): ReactElement {
  return <div />; // not assignable from arbitrary JSX in modern React
}

Best practices for function return types:

  • React component functions: annotate as ReactNode, or omit the annotation entirely and let TypeScript infer it. Inference usually picks the right type and adapts if you change the return.
  • Helper functions that always return a single element: JSX.Element is appropriate because you control the return and the narrowness is desirable.
  • Helper functions that may return null, a string, or a number: use ReactNode.
  • Render prop functions: choose JSX.Element if you require JSX, ReactNode if you permit non-JSX returns.
  • Avoid ReactElement as a return type on components or helpers. It’s a concrete object type, not a renderable value type, and it doesn’t match what JSX expressions produce in modern React.

A useful shortcut: in most cases, you can simply omit the return type annotation on React components. TypeScript will infer a JSX.Element for the happy path and null/undefined for early returns, and the resulting inferred type is structurally compatible with ReactNode. The annotation only matters when you want to constrain what the function may return — which is exactly the situation where ReactNode (broad) or JSX.Element (narrow) gives you the behavior you want.

Check it#

bash
tsc --noEmit

If you previously had TS2322 on null, string, or array returns, those are gone now.

If you’re still seeing errors, two common variants exist:

Variant A — Using ReactElement instead of ReactNode#

You might see:

plaintext
TS2322: Type 'Element' is not assignable to type 'ReactElement'.

This happens when you type a prop as ReactElement but pass JSX (which TypeScript infers as Element, not ReactElement). Fix it by switching to ReactNode — or, if you must use ReactElement, explicitly cast with as ReactElement (but avoid this unless you control the source).

Variant B — Strict JSX mode in Next.js or Vite#

In some setups (like Next.js 13+ with the "jsx": "react-jsx" transform), JSX.Element is aliased to React.JSX.Element, which is slightly different from React.JSX.Element in older versions. If you’re mixing libraries (e.g., React 18 + old @types/react), you might get TS2322 on valid JSX.

Fix it by ensuring @types/react and react versions match (e.g., both v18.3+), and use ReactNode universally — it’s the most stable cross-version choice.

Going forward#

The confusion is mostly historical: early React docs leaned on ReactElement loosely, and JSX.Element got cargo-culted as a “safe” type. ReactNode is the right one for almost everything — it’s what React.FC used to use (though React.FC itself is now discouraged).

Rules to stop reaching for the wrong type:

  • For return types: Always use ReactNode (or omit the return type entirely — TypeScript infers it correctly).
  • For props: Use ReactNode for children, fallback, empty, render, childrenRenderer, etc. Use JSX.Element only for render props where you want to enforce JSX-only input (e.g., <DataTable renderHeader={() => <h1 />} />).
  • For libraries: If you’re publishing a component library, explicitly type props with ReactNode — it’s the most compatible choice across React versions.

I cover this in detail in JS to TypeScript: Incremental Migration, No Full Rewrite, where we refactor legacy React components without breaking existing behavior.

FAQ#

What’s the difference between JSX.Element and ReactElement in practice?#

JSX.Element is the type that JSX expressions compile to — it’s a compile-time construct. ReactElement is the runtime object created by React.createElement(). In practice, JSX.Element is often aliased to ReactElement, but in modern React, JSX.Element is intentionally narrow, while ReactElement is a specific object shape. You rarely need ReactElement directly — and using it as a prop type is almost always a mistake.

When should I use ReactNode instead of JSX.Element as a return type?#

Use ReactNode for all component return types unless you have a strong reason to restrict rendering to JSX only (e.g., a strict render prop API). ReactNode supports null, strings, numbers, arrays, and fragments — which covers every valid React render scenario. JSX.Element only supports single JSX elements, so it fails on null, strings, and arrays.

How should I type component props in TypeScript React components?#

For component props that accept renderable content (children, header, footer, fallback, render, icon, overlay, tooltip, etc.), use ReactNode. It accepts null, strings, numbers, arrays, and JSX — everything a render can produce. Use JSX.Element only for render prop functions where you want to enforce a JSX-only return. Avoid ReactElement as a prop type: it’s a runtime object shape ({ type, props, key }), not a renderable value, and it’s not assignable from arbitrary JSX in modern React.

What is the best function return type for React components in TypeScript?#

Use ReactNode as the function return type for React components, or omit the annotation entirely so TypeScript infers it. ReactNode covers null, strings, numbers, arrays, fragments, and JSX — every legal render output. JSX.Element is too narrow for components because it rejects null and primitives, which is the most common cause of TS2322 on component return types.

How do I fix 'Type is not assignable to type JSX.Element' errors?#

First, check if the value is null, a string, a number, or an array — if so, replace JSX.Element with ReactNode. If the value is a ReactElement (e.g., from React.cloneElement), you likely need ReactNode or ReactElement — but ReactNode is safer for most use cases. If you’re using React.cloneElement, type the result as ReactElement, not JSX.Element.

What return type should I use for components that might return null?#

Always ReactNode. ReactNode explicitly includes null and undefined — it’s designed for conditional rendering. JSX.Element does not, which is why return null fails with JSX.Element.

How do I properly type children props in React components?#

Use { children: ReactNode }. This is the official React pattern and matches how React.createElement handles children. Avoid JSX.Element or ReactElement — they exclude strings, numbers, and arrays, which are common children values.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.