is a union that includes ReactElement, strings, numbers, booleans, null, undefined, arrays, portals, and fragments.
Step 3: Reserve JSX.Element for Strict Contracts
Use JSX.Element only when you need to enforce that a function returns valid JSX syntax. This is appropriate for render props where the consumer must provide a structured element, and returning null or a string would break the layout.
Step 4: Avoid ReactElement in Signatures
ReactElement describes the shape of the object created by React.createElement(). It is an implementation detail. Using it in props or returns restricts consumers to passing specific object shapes and often causes type mismatches with JSX expressions in modern React.
Implementation Examples
Example 1: Flexible Content Props
import type { ReactNode } from 'react';
interface NotificationBannerProps {
message: string;
icon?: ReactNode;
actions?: ReactNode;
onClose?: () => void;
}
export function NotificationBanner({
message,
icon,
actions,
onClose,
}: NotificationBannerProps) {
return (
<div className="banner">
{icon && <span className="icon">{icon}</span>}
<p>{message}</p>
{actions && <div className="actions">{actions}</div>}
{onClose && <button onClick={onClose}>Dismiss</button>}
</div>
);
}
Rationale: icon and actions are typed as ReactNode. This allows consumers to pass JSX elements, strings, numbers, or even null/undefined safely. If icon were JSX.Element, passing a string icon name or a conditional null would trigger TS2322.
Example 2: Data Grid with Mixed Render Contracts
import type { ReactNode } from 'react';
interface DataTableProps<T> {
data: T[];
columns: string[];
renderRow: (item: T) => ReactNode;
renderEmpty?: () => ReactNode;
renderHeader: (column: string) => JSX.Element;
}
export function DataTable<T>({
data,
columns,
renderRow,
renderEmpty,
renderHeader,
}: DataTableProps<T>) {
if (data.length === 0) {
return renderEmpty ? renderEmpty() : <div>No data available</div>;
}
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={col}>{renderHeader(col)}</th>
))}
</tr>
</thead>
<tbody>
{data.map((item, index) => (
<tr key={index}>{renderRow(item)}</tr>
))}
</tbody>
</table>
);
}
Rationale:
renderRow returns ReactNode because a row might conditionally render null to hide data, or return a string/number directly.
renderEmpty returns ReactNode to allow returning null or a fragment.
renderHeader returns JSX.Element to enforce that headers must be JSX elements, ensuring consistent table structure.
Example 3: Component Return Types
import type { ReactNode } from 'react';
interface UserProfileProps {
userId: string;
}
export function UserProfile({ userId }: UserProfileProps): ReactNode {
// Simulate loading state
const isLoading = false;
const user = { name: 'Alice' };
if (isLoading) return null;
if (!user) return <div>User not found</div>;
return (
<div className="profile">
<h2>{user.name}</h2>
</div>
);
}
Rationale: The return type is ReactNode. This permits returning null during loading, a string/JSX on error, and JSX on success. Annotating with JSX.Element would reject the null return, causing TS2322.
Pitfall Guide
1. The ReactElement Prop Anti-Pattern
Explanation: Developers sometimes type props as ReactElement, believing it represents "a React element." However, ReactElement is a runtime object shape ({ type, props, key }). JSX expressions in modern React are inferred as JSX.Element, which may not be assignable to ReactElement depending on the version and configuration.
Fix: Replace ReactElement with ReactNode for props. If you need to restrict to elements only, use JSX.Element, but prefer ReactNode for maximum compatibility.
2. Return Type Over-Constraint
Explanation: Annotating component functions with (): JSX.Element when the body can return null, strings, or arrays. This is a common mistake when developers try to be explicit but choose the wrong type.
Fix: Use ReactNode for return types, or omit the annotation entirely. TypeScript's inference often correctly infers the union type based on return statements. Explicit annotation should only be used to constrain the return, in which case ReactNode is the correct broad type.
3. Render Prop Rigidity
Explanation: Typing render props like renderItem or renderEmpty as JSX.Element when the implementation might return null or a primitive. This forces consumers to wrap returns in fragments or disable checks.
Fix: Analyze the render prop's behavior. If it can return non-JSX values, type it as ReactNode. Use JSX.Element only when the render prop must produce JSX structure for layout integrity.
4. Array and Fragment Blindness
Explanation: Assuming that JSX.Element covers arrays of elements or fragments. JSX.Element represents a single JSX expression. Arrays and fragments are handled by ReactNode.
Fix: Use ReactNode for props or returns that may contain arrays or fragments. ReactNode includes ReactNode[] and ReactFragment, ensuring arrays map correctly.
5. @types/react Version Mismatch
Explanation: Errors like Type 'Element' is not assignable to type 'ReactElement' can occur when @types/react versions are misaligned with the react package, or when mixing libraries with different type definitions.
Fix: Ensure react and @types/react versions match. Use ReactNode universally in your codebase, as it is the most stable type across versions. Run npm ls @types/react to check for duplicates or mismatches.
6. Legacy React.FC Reliance
Explanation: Using React.FC (FunctionComponent) can introduce implicit children typing and other quirks that conflict with modern practices. React.FC is discouraged in favor of explicit interfaces.
Fix: Avoid React.FC. Define explicit interfaces for props and use standard function declarations or arrow functions with ReactNode returns. This gives you full control over prop types and avoids hidden behaviors.
7. Strict JSX Mode Configuration
Explanation: In setups with "jsx": "react-jsx" (automatic runtime), JSX.Element may have different aliases or behaviors compared to classic mode. Mixing libraries or configurations can lead to type incompatibilities.
Fix: Verify tsconfig.json settings. Ensure consistent JSX transform across the project. Use ReactNode to abstract away JSX-specific type differences, as it remains stable regardless of JSX mode.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
children prop | ReactNode | Universal compatibility; accepts all renderable values. | Low |
header/footer slots | ReactNode | Allows strings, JSX, or conditional rendering. | Low |
renderRow in table | ReactNode | Rows may return null or primitives; flexibility needed. | Low |
renderHeader in table | JSX.Element | Enforces JSX structure for consistent column headers. | Low |
error fallback | ReactNode | Can be string message, JSX, or null. | Low |
| Component return | ReactNode or Omit | Supports conditional returns; inference works well. | Low |
| Internal helper | Omit annotation | Reduces boilerplate; inference adapts to changes. | Low |
| Library public API | ReactNode | Maximizes consumer flexibility; version-stable. | Low |
Configuration Template
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}
Notes:
"jsx": "react-jsx" enables the automatic JSX runtime, recommended for React 17+.
"strict": true ensures robust type checking.
"skipLibCheck": true can help avoid type errors from third-party libraries, though aligning versions is preferred.
Reusable Type Definitions (types/react.d.ts)
import type { ReactNode } from 'react';
// Global utility types for consistent usage
type RenderableContent = ReactNode;
type StrictJSX = JSX.Element;
// Example interface pattern
interface WithChildren {
children?: ReactNode;
}
interface WithRenderProp<T> {
renderItem: (item: T) => ReactNode;
}
Quick Start Guide
- Install Dependencies: Ensure
react and @types/react are installed and versions match.
npm install react @types/react
- Define Interface: Create a component interface using
ReactNode for content props.
import type { ReactNode } from 'react';
interface MyComponentProps {
title: string;
content: ReactNode;
footer?: ReactNode;
}
- Implement Component: Build the component, allowing flexible returns and props.
export function MyComponent({ title, content, footer }: MyComponentProps) {
return (
<div>
<h1>{title}</h1>
<main>{content}</main>
{footer && <footer>{footer}</footer>}
</div>
);
}
- Validate Types: Run the TypeScript compiler to confirm no errors.
npx tsc --noEmit
- Refactor Existing Code: Apply
ReactNode to existing components with TS2322 errors, replacing JSX.Element where flexibility is needed.