Children in React can be almost anything: strings, numbers, JSX elements, arrays of elements, fragments, or even null. But when you’re writing TypeScript, how do you type that children prop without breaking half your use cases or being overly permissive?
The secret lies in understanding three key types: ReactNode, ReactElement, and JSX.Element. They might look interchangeable, but choosing the wrong one will either break your component’s flexibility or give consumers confusing type errors. Once you understand their hierarchy and purpose, you’ll never second-guess children props again.
The Big Picture
Before diving into the specifics, here’s the mental model you need:
ReactNode: Anything React can render (most permissive)ReactElement: The result of JSX expressions (middle ground)JSX.Element: TypeScript’s specific representation of JSX (most restrictive)
Most of the time, you’ll want ReactNode for children props and ReactElement or JSX.Element for component return types. But let’s see why.
ReactNode: The Swiss Army Knife
ReactNode is the most permissive type—it represents anything that React can render. This includes strings, numbers, elements, arrays, fragments, and even null or undefined.
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined;This makes it perfect for typing children props, since parents often need to accept a wide variety of content:
// ✅ Good: ReactNode accepts anything renderable
interface CardProps {
children: ReactNode;
}
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
// All of these work:
<Card>Hello world</Card>
<Card>{42}</Card>
<Card><Button>Click me</Button></Card>
<Card>{[<span key="1">Item 1</span>, <span key="2">Item 2</span>]}</Card>
<Card>{null}</Card>Compare this with the more restrictive alternatives:
// ❌ Too restrictive: only accepts JSX elements
interface CardProps {
children: ReactElement;
}
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
// Type errors!
<Card>Hello world</Card> // ❌ string not assignable to ReactElement
<Card>{42}</Card> // ❌ number not assignable to ReactElement
<Card>{null}</Card> // ❌ null not assignable to ReactElementReactNode. It gives component consumers maximum flexibility while still ensuring type safety.
Children Patterns in the Wild
Now that you understand ReactNode, let’s look at the most common children patterns you’ll encounter and how to type them properly.
The Basic Container
Most wrapper components need to accept any renderable content:
interface CardProps {
children: ReactNode;
className?: string;
}
function Card({ children, className }: CardProps) {
return (
<div className={`card ${className || ''}`}>
{children}
</div>
);
}
// All of these just work:
<Card>Simple text</Card>
<Card><h1>Title</h1><p>Content</p></Card>
<Card>{user ? <Profile user={user} /> : 'Please log in'}</Card>
<Card>{items.map(item => <Item key={item.id} item={item} />)}</Card>Optional Children
Sometimes children are optional, and you want to conditionally render wrapper elements:
interface SectionProps {
title: string;
children?: ReactNode; // Optional children
}
function Section({ title, children }: SectionProps) {
return (
<section>
<h2>{title}</h2>
{children && <div className="content">{children}</div>}
</section>
);
}
// Both of these work:
<Section title="Empty section" />
<Section title="With content">
<p>Some content here</p>
</Section>Multiple Children Slots
Modern React patterns often use multiple “slots” for different types of content:
interface LayoutProps {
header: ReactNode;
sidebar: ReactNode;
children: ReactNode;
footer?: ReactNode;
}
function Layout({ header, sidebar, children, footer }: LayoutProps) {
return (
<div className="layout">
<header>{header}</header>
<div className="main">
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
{footer && <footer>{footer}</footer>}
</div>
);
}
// Usage allows maximum flexibility:
<Layout
header={<Navigation />}
sidebar="Simple text sidebar"
footer={null} // Explicitly no footer
>
<Article />
</Layout>;Render Props with Children
When you’re building components that use render props, ReactNode keeps things flexible:
interface DataFetcherProps {
url: string;
children: (data: any, loading: boolean) => ReactNode;
}
function DataFetcher({ url, children }: DataFetcherProps) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [url]);
return <>{children(data, loading)}</>;
}
// The render function can return anything renderable:
<DataFetcher url="/api/users">
{(users, loading) => {
if (loading) return 'Loading...';
if (!users) return null;
return users.map((user) => <UserCard key={user.id} user={user} />);
}}
</DataFetcher>;Conditional Children Rendering
One of the most common patterns is conditionally rendering children based on state:
interface CollapsibleProps {
title: string;
children: ReactNode;
defaultOpen?: boolean;
}
function Collapsible({ title, children, defaultOpen = false }: CollapsibleProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="collapsible">
<button onClick={() => setIsOpen(!isOpen)}>
{title} {isOpen ? '▼' : '▶'}
</button>
{isOpen && <div className="collapsible-content">{children}</div>}
</div>
);
}
// Works with any content that might be conditionally hidden:
<Collapsible title="Advanced Options">
<form>
<input type="text" placeholder="Advanced setting" />
<button type="submit">Save</button>
</form>
</Collapsible>;Working with Children: React’s Helper Functions
When you need to manipulate or inspect children, React provides several utility functions. Here’s how to use them with proper TypeScript typing:
React.Children.map
When you need to transform each child element:
import { ReactNode, cloneElement, isValidElement } from 'react';
interface WrapperProps {
children: ReactNode;
}
function AddClassToChildren({ children }: WrapperProps) {
return (
<div>
{React.Children.map(children, (child, index) => {
// Type guard to ensure we have a valid React element
if (isValidElement(child)) {
return cloneElement(child, {
className: `${child.props.className || ''} wrapped-${index}`.trim(),
});
}
// Return non-element children (strings, numbers) unchanged
return child;
})}
</div>
);
}React.Children.count
Count renderable children (ignores null, undefined, and booleans):
interface ListProps {
children: ReactNode;
showCount?: boolean;
}
function List({ children, showCount }: ListProps) {
const count = React.Children.count(children);
return (
<ul>
{showCount && <li className="count">Total items: {count}</li>}
{React.Children.map(children, (child, index) => (
<li key={index}>{child}</li>
))}
</ul>
);
}React.Children.only
When you need exactly one child element:
interface SingleChildProps {
children: ReactNode;
}
function SingleChildWrapper({ children }: SingleChildProps) {
// This will throw if children is not exactly one element
const singleChild = React.Children.only(children);
if (isValidElement(singleChild)) {
return cloneElement(singleChild, {
className: `${singleChild.props.className || ''} enhanced`.trim()
});
}
return singleChild;
}
// ✅ Works:
<SingleChildWrapper>
<Button>Click me</Button>
</SingleChildWrapper>
// ❌ Throws error (multiple children):
<SingleChildWrapper>
<Button>One</Button>
<Button>Two</Button>
</SingleChildWrapper>Type Guards for Children
Sometimes you need to differentiate between different types of children:
function isReactElement(child: ReactNode): child is ReactElement {
return isValidElement(child);
}
function isStringChild(child: ReactNode): child is string {
return typeof child === 'string';
}
interface SmartListProps {
children: ReactNode;
}
function SmartList({ children }: SmartListProps) {
return (
<ul>
{React.Children.map(children, (child, index) => {
if (isStringChild(child)) {
return (
<li key={index} className="text-item">
{child}
</li>
);
}
if (isReactElement(child)) {
return (
<li key={index} className="element-item">
{child}
</li>
);
}
// Handle other types (numbers, etc.)
return (
<li key={index} className="other-item">
{String(child)}
</li>
);
})}
</ul>
);
}React.Children.only—it throws an error if you don’t have exactly one child. Always use it when you specifically need that constraint, not as a general children handler.
ReactElement: The JSX Result
ReactElement represents the objects that JSX expressions create—the virtual DOM nodes that React uses internally. It’s more specific than ReactNode but more flexible than JSX.Element.
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>,
> {
type: T;
props: P;
key: Key | null;
}You’ll typically use ReactElement when you need to ensure you’re getting an actual element (not a string or number), but you want to accept any kind of element:
interface IconButtonProps {
icon: ReactElement;
label: string;
}
function IconButton({ icon, label }: IconButtonProps) {
return (
<button>
{icon}
<span>{label}</span>
</button>
);
}
// ✅ Works with any JSX element
<IconButton icon={<HomeIcon />} label="Home" />
<IconButton icon={<div>📁</div>} label="Folder" />
// ❌ Doesn't work with strings or numbers
<IconButton icon="🏠" label="Home" /> // Type errorThis is particularly useful for component slots or when you need to clone or manipulate elements:
interface ModalProps {
trigger: ReactElement;
children: ReactNode;
}
function Modal({ trigger, children }: ModalProps) {
const [isOpen, setIsOpen] = useState(false);
// We can clone the trigger and add props because it's a ReactElement
const triggerWithClick = cloneElement(trigger, {
onClick: () => setIsOpen(true),
});
return (
<>
{triggerWithClick}
{isOpen && (
<div className="modal">
{children}
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
)}
</>
);
}
// Usage:
<Modal trigger={<Button>Open Modal</Button>}>
<p>Modal content here</p>
</Modal>;ReactElement vs JSX.Element vs ReactNode: The Complete Picture
Now that you understand the basics, let’s dive deeper into when and why you’d choose each type. These three types form a hierarchy, and knowing their exact differences will make you a TypeScript React power user.
JSX.Element: TypeScript’s Take
JSX.Element is TypeScript’s specific type for JSX expressions. It’s essentially an alias for ReactElement<any, any>, but it’s what TypeScript infers when you write JSX:
// TypeScript infers JSX.Element for these expressions:
const element1 = <div>Hello</div>; // JSX.Element
const element2 = <Button>Click</Button>; // JSX.ElementThe key insight: JSX.Element is TypeScript’s representation, while ReactElement is React’s. They’re nearly identical, but JSX.Element has a crucial limitation:
// JSX.Element is always ReactElement<any, any>
type JSXElement = ReactElement<any, any>;
// This means JSX.Element loses generic type information:
function createTypedElement(): ReactElement<{ label: string }> {
return <button>Click</button>; // Preserves prop types
}
function createJSXElement(): JSX.Element {
return <button>Click</button>; // Props become 'any'
}ReactElement: The Middle Ground
ReactElement represents the objects that JSX expressions create—the virtual DOM nodes that React uses internally. It’s more specific than ReactNode but more flexible than JSX.Element:
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>,
> {
type: T;
props: P;
key: Key | null;
}
// ReactElement can preserve type information
const typedElement: ReactElement<ButtonProps> = <Button variant="primary">Click</Button>;
// You can extract props from a ReactElement
type ExtractedProps = typeof typedElement.props; // ButtonPropsThe Type Hierarchy
Here’s the critical relationship to understand:
// ReactNode is the superset of everything React can render
type ReactNode =
| ReactElement // ← This includes JSX.Element
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined;
// JSX.Element is a specific ReactElement
type JSXElement = ReactElement<any, any>;
// The hierarchy:
// ReactNode > ReactElement > JSX.ElementWhen to Use Each: A Decision Tree
// Question 1: Are you typing children props?
// → Use ReactNode (99% of cases)
interface ContainerProps {
children: ReactNode; // ✅ Maximum flexibility
}
// Question 2: Do you need to clone or inspect the element?
// → Use ReactElement
interface ModalProps {
trigger: ReactElement; // ✅ Can clone and add props
children: ReactNode; // ✅ Just rendering
}
function Modal({ trigger, children }: ModalProps) {
// Can safely clone because trigger is ReactElement
const enhancedTrigger = cloneElement(trigger, {
onClick: () => setOpen(true),
});
return (
<>
{enhancedTrigger}
{children}
</>
);
}
// Question 3: Are you constraining return types?
// → Let TypeScript infer or use ReactNode for conditionals
function ConditionalComponent({ show }: { show: boolean }): ReactNode {
if (!show) return null; // ✅ ReactNode allows null
return <div>Content</div>;
}
// Question 4: Do you need specific prop types preserved?
// → Use ReactElement with generics
function processElement(element: ReactElement<{ className?: string }>) {
// TypeScript knows element.props has className
const className = element.props.className || 'default';
return cloneElement(element, { className: `${className} processed` });
}Real-World Example: Type-Safe Component Slots
Here’s a pattern that shows the practical differences:
interface LayoutProps {
// ReactNode: Just render as-is
header: ReactNode;
footer: ReactNode;
// ReactElement: Need to manipulate
sidebar: ReactElement;
// Specific ReactElement: Type-safe props
navigation: ReactElement<{ isActive?: boolean }>;
// Main content
children: ReactNode;
}
function Layout({ header, footer, sidebar, navigation, children }: LayoutProps) {
// Can render ReactNode directly
const headerContent = header; // Could be string, element, null, etc.
// Can clone and enhance ReactElement
const enhancedSidebar = cloneElement(sidebar, {
className: 'layout-sidebar'
});
// Can access specific props on typed ReactElement
const navWithHighlight = cloneElement(navigation, {
isActive: true,
className: navigation.props.isActive ? 'nav-active' : 'nav'
});
return (
<div>
<header>{headerContent}</header>
<nav>{navWithHighlight}</nav>
<aside>{enhancedSidebar}</aside>
<main>{children}</main>
<footer>{footer}</footer>
</div>
);
}
## The Building Blocks: ReactChild, ReactFragment, and ReactPortal
While `ReactNode` is the umbrella type for everything React can render, it's actually composed of several more specific types. Understanding these building blocks helps you write more precise types when needed and debug type errors more effectively.
### ReactChild: The Simple Renderable
`ReactChild` represents the simplest things React can render—individual primitive values or elements:
```tsx
// ReactChild is a union of the basic renderable units
type ReactChild = ReactElement | string | number;
// These are all valid ReactChild values:
const textChild: ReactChild = "Hello World";
const numberChild: ReactChild = 42;
const elementChild: ReactChild = <div>Content</div>;
// But these are NOT ReactChild:
const nullChild: ReactChild = null; // ❌ Not included
const arrayChild: ReactChild = [1, 2, 3]; // ❌ Arrays not included
const boolChild: ReactChild = true; // ❌ Booleans not included[!NOTE]
ReactChildis deprecated in newer React types. You should useReactNodeor be more specific withReactElement | string | numberinstead.
ReactFragment: Arrays and Keyed Children
ReactFragment represents React’s ability to render multiple children without a wrapper element:
// ReactFragment represents grouped children
type ReactFragment = Iterable<ReactNode>;
// Using fragments in practice
function ItemList({ items }: { items: string[] }) {
// Explicit fragment with key
return (
<>
{items.map((item, index) => (
<React.Fragment key={index}>
<dt>{item}</dt>
<dd>Description of {item}</dd>
</React.Fragment>
))}
</>
);
}
// ReactFragment in component props
interface ListProps {
// When you specifically need an array of nodes
items: ReactFragment;
// vs ReactNode which could be a single item
header: ReactNode;
}
function List({ items, header }: ListProps) {
return (
<div>
<h2>{header}</h2>
<ul>
{React.Children.map(items, (item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}The key difference: ReactFragment is specifically for collections, while ReactNode can be a single item or a collection:
// ReactNode: Single item OR array
const node1: ReactNode = 'Single string';
const node2: ReactNode = ['Array', 'of', 'strings'];
// ReactFragment: Always represents a collection
const fragment1: ReactFragment = ['Array', 'of', 'items'];
const fragment2: ReactFragment = new Set([1, 2, 3]); // Any iterable worksReactPortal: Rendering Outside the Tree
ReactPortal is the type returned by ReactDOM.createPortal, allowing you to render children into a different DOM subtree:
import { createPortal } from 'react-dom';
import type { ReactPortal, ReactNode } from 'react';
interface ModalProps {
children: ReactNode;
isOpen: boolean;
}
function Modal({ children, isOpen }: ModalProps): ReactPortal | null {
if (!isOpen) return null;
// createPortal returns a ReactPortal
return createPortal(
<div className="modal-backdrop">
<div className="modal-content">{children}</div>
</div>,
document.getElementById('modal-root')!,
);
}
// When you need to type portal-specific behavior
function PortalManager({ portals }: { portals: ReactPortal[] }) {
// ReactPortal has specific properties
return (
<>
{portals.map((portal) => {
// Each portal has key, children, containerInfo
console.log('Portal container:', portal.containerInfo);
return portal;
})}
</>
);
}Understanding the Complete ReactNode Union
Now you can see how ReactNode is built from these pieces:
// The full ReactNode type decomposed
type ReactNode =
| ReactElement // JSX elements
| string // Text nodes
| number // Numeric values
| Iterable<ReactNode> // Arrays and fragments (includes ReactFragment)
| ReactPortal // Portal nodes
| boolean // Conditionals (rendered as nothing)
| null // Absence of content
| undefined; // Undefined values
// This is why ReactNode is so flexible:
function FlexibleComponent({ content }: { content: ReactNode }) {
// content could be ANY of the above types
return <div>{content}</div>;
}
// All of these work:
<FlexibleComponent content="text" />
<FlexibleComponent content={42} />
<FlexibleComponent content={<span>element</span>} />
<FlexibleComponent content={[1, 2, 3]} />
<FlexibleComponent content={createPortal(<div />, document.body)} />
<FlexibleComponent content={null} />
<FlexibleComponent content={true && <div>Conditional</div>} />When to Use These Specific Types
While you’ll use ReactNode 95% of the time, knowing these specific types helps in certain scenarios:
// When you're building a table and need pairs
interface TableRowProps {
// Ensures we get an array, not a single element
cells: ReactFragment;
}
// When handling portal-specific logic
interface OverlayManagerProps {
// Specifically portal elements
overlays: ReactPortal[];
// Regular children
children: ReactNode;
}
// When you need to exclude certain types
type TextOnly = Extract<ReactNode, string | number>;
interface TextDisplayProps {
// Only accepts text or numbers, not elements
content: TextOnly;
}
function TextDisplay({ content }: TextDisplayProps) {
// TypeScript knows content is string | number
return <span className="text-only">{String(content)}</span>;
}ReactNode unless you have a specific reason to be more restrictive. The specific types are mainly useful for library authors or when building highly specialized components.
Real-World Guidelines
Here’s how to choose between these types in common scenarios:
Children Props: Almost Always ReactNode
// ✅ Good: Flexible children prop
interface Props {
children: ReactNode;
}
// ❌ Too restrictive for most use cases
interface Props {
children: ReactElement;
}Render Props: Usually ReactNode
interface Props {
renderHeader: () => ReactNode; // ✅ Flexible
renderFooter: () => JSX.Element; // ❌ Too restrictive
}
function Layout({ renderHeader, renderFooter }: Props) {
return (
<div>
<header>{renderHeader()}</header>
<main>Content</main>
<footer>{renderFooter()}</footer>
</div>
);
}
// With ReactNode, both of these work:
<Layout
renderHeader={() => 'Simple string header'}
renderFooter={() => <div>Complex footer</div>}
/>;Element Props: ReactElement When You Need Manipulation
interface Props {
// Use ReactElement when you need to clone or inspect the element
trigger: ReactElement;
// Use ReactNode when you just need to render it
icon: ReactNode;
}
function Dropdown({ trigger, icon }: Props) {
// Can clone trigger because it's guaranteed to be an element
const enhancedTrigger = cloneElement(trigger, {
'aria-expanded': isOpen,
});
return (
<div>
{icon} {/* Just render the icon */}
{enhancedTrigger}
</div>
);
}Component Return Types: Let TypeScript Infer
// ✅ Let TypeScript infer JSX.Element
function Button() {
return <button>Click me</button>;
}
// ⚠️ Explicit typing usually unnecessary
function Button(): JSX.Element {
return <button>Click me</button>;
}
// ✅ Explicit typing when returning conditional content
function ConditionalButton({ show }: { show: boolean }): ReactNode {
if (!show) return null;
return <button>Click me</button>;
}Common Pitfalls and Solutions
Pitfall 1: Using ReactElement for Children
// ❌ This breaks with strings, numbers, arrays
interface Props {
children: ReactElement;
}
// ✅ Use ReactNode instead
interface Props {
children: ReactNode;
}Pitfall 2: Forgetting About Conditional Rendering
// ❌ This can return null, but JSX.Element doesn't allow it
function ConditionalComponent({ show }: { show: boolean }): JSX.Element {
if (!show) return null; // Type error!
return <div>Visible</div>;
}
// ✅ Use ReactNode for conditional rendering
function ConditionalComponent({ show }: { show: boolean }): ReactNode {
if (!show) return null;
return <div>Visible</div>;
}Pitfall 3: Over-Constraining Render Functions
// ❌ Too restrictive—breaks with conditional rendering
interface Props {
renderItem: (item: Item) => JSX.Element;
}
// ✅ More flexible
interface Props {
renderItem: (item: Item) => ReactNode;
}The Children Typing Decision Tree
Here’s your go-to guide for typing children and related props:
For children props (95% of cases):
// ✅ Almost always correct
interface Props {
children: ReactNode;
}When children are optional:
// ✅ Use optional ReactNode
interface Props {
children?: ReactNode;
}When you need to manipulate children:
// ✅ Use ReactNode, then type-guard with isValidElement
interface Props {
children: ReactNode;
}
function MyComponent({ children }: Props) {
return React.Children.map(children, (child) => {
if (isValidElement(child)) {
// Now you can safely use cloneElement, etc.
return cloneElement(child, {
/* additional props */
});
}
return child;
});
}For render prop functions:
// ✅ Return ReactNode for maximum flexibility
interface Props {
render: (data: Data) => ReactNode;
}For element-specific props (rare):
// ✅ Only when you specifically need an element
interface Props {
trigger: ReactElement; // Will be cloned/enhanced
children: ReactNode; // Will be rendered as-is
}The Bottom Line
When typing children, think in this order:
- Start with
ReactNode— it handles 95% of use cases correctly - Consider
ReactElementonly when you need to manipulate the element (clone, inspect props, etc.) - Avoid
JSX.Elementfor children — it’s too restrictive and doesn’t add value
The React ecosystem is built on flexibility and composition. Your typing should reflect that philosophy. When you use ReactNode for children, you’re allowing consumers to pass strings, elements, arrays, conditional content, or even null—exactly what React was designed to handle.
Don’t overthink it: if it’s called children and goes between JSX tags, type it as ReactNode. Your future self (and your component’s users) will thank you for the flexibility.