JSX.Element vs ReactNode vs ReactElement: TS2322 Fix
Fix TS2322 by understanding when ReactNode, JSX.Element, and ReactElement apply in React + TypeScript component typing.
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:
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. Checked directly against the current @types/react source on DefinitelyTyped, JSX.Element is defined as interface Element extends React.ReactElement<any, any> {} — it has always been exactly ReactElement<any, any>, not a type that got "narrowed" in a later React version. What narrows it is simpler: ReactElement itself has never included null, strings, numbers, or arrays, so neither does JSX.Element.
ReactElement, on the other hand, is the TypeScript interface describing the 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, since props typed ReactElement reject the same null/string/number/array values that JSX.Element rejects.
ReactNode is the union type React uses internally to define what can be rendered. In the current @types/react source it's ReactElement | string | number | bigint | Iterable<ReactNode> | ReactPortal | boolean | null | undefined (plus an Awaited<ReactNode>-flavoured branch for async Server Components) — in short, every legal render output, not just a single JSX element. It’s the correct type for component return values and most props that accept content — the official React TypeScript guide recommends ReactNode for a children prop precisely because of this broad definition, in contrast to the narrower ReactElement.
// src/components/Card.tsx
import React from 'react';
// ❌ Wrong: JSX.Element excludes null, strings, numbers, arrays
function Card({ title, content }: { title: string; content: string }): React.JSX.Element {
if (!content) {
return null; // ← TS2322: Type 'null' is not assignable to type '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[] }): React.JSX.Element {
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).
// src/components/Card.tsx
import React, { ReactNode } from 'react';
// ✅ Correct: ReactNode accepts null, strings, numbers, arrays
function Card({ title, content }: { title: string; content: string }): ReactNode {
if (!content) {
return null;
}
return (
<div>
<h2>{title}</h2>
<p>{content}</p>
</div>
);
}
// ✅ Also correct: ReactNode handles arrays naturally
function List({ items }: { items: string[] }): ReactNode {
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#
- Open the file where you see the TS2322 error.
- Find the component or function with the incorrect type (e.g.,
(): JSX.Elementor{ render: JSX.Element }). - Replace
JSX.ElementwithReactNodein return types and props likechildren,fallback,empty, orrender. - Save and run
tsc --noEmitto 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.
// 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:
childrenand any "slot" prop (header,footer,body,fallback,empty,overlay,tooltip): useReactNode. Optional slots should beReactNode | undefined(or justReactNode— it already includesundefined).- Render props where the consumer must return JSX (
renderRow,renderHeader,renderCell): useJSX.Elementto enforce a strict JSX return. - Render props that may return a string, number, or
null(renderLabel,renderEmpty): useReactNode. - Avoid
ReactElementas a prop type. It’s a runtime object shape ({ type, props, key }) — consumers don’t think in those terms, and (beingReactElement, same asJSX.Element) it rejectsnull, strings, numbers, and arrays just asJSX.Elementdoes.
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:
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 GreetingStrict({ 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, not JSX.Element
function MaybeSpinner({ loading }: { loading: boolean }): ReactNode {
if (!loading) return null;
return <Spinner />;
}
// ❌ Don't use ReactElement as a return type on components — it's a concrete
// object shape, not idiomatic for rendering, even though JSX does structurally fit it
function Broken(): ReactElement {
return <div />;
}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.Elementis appropriate because you control the return and the narrowness is desirable. - Helper functions that may return
null, a string, or a number: useReactNode. - Render prop functions: choose
JSX.Elementif you require JSX,ReactNodeif you permit non-JSX returns. - Avoid
ReactElementas a return type on components or helpers. It’s a concrete object type, not a renderable value type — it technically accepts the JSX you return (JSX.Elementis itselfReactElement<any, any>), but it carries the samenull/string/array gaps asJSX.Elementwith none of its readability.
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#
tsc --noEmitIf 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 — Duplicate or mismatched @types/react copies#
You might see something like:
Type 'Element' is not assignable to type 'ReactElement'.
Two different types with this name exist, but they are unrelated.Checked against a current tsc run, passing plain JSX to a prop or return typed ReactElement does not fail on its own — JSX.Element is ReactElement<any, any>, so it's structurally the same thing. This error instead comes from having two separate installs of @types/react in node_modules (common in monorepos, or when a dependency pins its own @types/react), so TypeScript sees two distinct ReactElement/JSX.Element declarations that happen to share a name. Fix it with npm ls @types/react to find the duplicate, then dedupe it (or add a single pinned version via your package manager's overrides/resolutions field) rather than casting around it.
Variant B — Bare JSX.Element with current @types/react#
As of @types/react 19 (the version shipping with React 19), the ambient global JSX namespace that older @types/react declared was removed in favour of React.JSX. Confirmed with a tsc --noEmit run against @types/react@19.3.0: a bare JSX.Element return type — even with import React from 'react' already in scope — now fails with TS2503: Cannot find namespace 'JSX', regardless of the jsx transform setting.
Fix it by qualifying the type as React.JSX.Element, or by importing it explicitly: import type { JSX } from 'react'. If you're on an older @types/react (18.x) the bare JSX.Element form still works, which is why this only shows up after an upgrade.
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
ReactNodeforchildren,fallback,empty,render,childrenRenderer, etc. UseJSX.Elementonly 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.
Further reading: How to Convert a String to Number in TypeScript: 2026 Fix, TypeScript ! Operator: Fix Object Is Possibly Undefined.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Cannot use JSX unless the '--jsx' flag is provided
The tsconfig jsx option is set, but the editor still reports the JSX flag error. Here's how to force VS Code to use the project's TypeScript version and apply the change.
Fix Cannot find module '@vitejs/plugin-react' in Vite
Read one line of the tsc output to tell a missing package from a resolution setting, then fix vite.config.ts in the tsconfig that actually owns it.
Fix TS7053: Element Implicitly Has an Any Type When Indexing
TypeScript throws TS7053 when a plain string indexes a typed object. Use keyof typeof, index signatures, or Record to fix it safely.


