The TypeScript typing ecosystem around React.FC and its relationship with the children prop has evolved significantly. Earlier patterns leaned on implicit typing of children, while modern recommendations prefer explicit and more predictable typings — both for everyday app components and for library-style, reusable building blocks.
Typing the children prop in React can feel perplexing initially. Trying to be “precise” by using narrow JSX types often leads to frustrating runtime issues or TypeScript errors when rendering nested components. On top of that, the sheer number of possible typings for children can cause unnecessary analysis paralysis.
This guide walks through practical, recommended ways to type the children prop correctly, and then extends into more advanced patterns around class components, components-as-props, wrapper components, and refs in React 18/19.
The core purpose of the children prop is to capture whatever content is passed between the opening and closing tags of a JSX element. Whenever you write a JSX tag pair, the material between them becomes that component’s children.
Consider:
<Border> Hey, I represent the JSX children! </Border>Here, the literal string Hey, I represent the JSX children! is the child rendered inside Border.
React exposes this content via the special props.children prop. For example:
const Border = ({ children }) => {
return (
<div style={{ border: '1px solid red' }}>
{children}
</div>
);
};
Border accepts children and simply renders it inside a bordered <div>.
Supported Children Types
React supports a variety of values as children. Some of the most common include:
Literal strings are perfectly valid
<YourComponent> This is a valid child string </YourComponent>Inside YourComponent, props.children will be the string:
"This is a valid child string"
You can also pass JSX elements, which is especially handy for composition:
<Wrapper>
I am a valid string child
<YourFirstComponent />
<YourSecondComponent />
</Wrapper>
Any JavaScript expression is allowed as children when wrapped in curly braces:
<YourFirstComponent>{myScopedVariableRef}</YourFirstComponent>myScopedVariableRef can be any expression: a string, number, JSX element, array, or even the result of a function call.
4. Functions
You can also pass functions (for example, render props):
<YourFirstComponent>
{() => <div>{myScopedVariableRef}</div>}
</YourFirstComponent>As seen above, the children prop can encompass a broad spectrum of values. The naive temptation is to type them manually:
type Props = {
children: string | JSX.Element | JSX.Element[] | () => JSX.Element;
};
const YourComponent = ({ children }: Props) => { return children;
};But this leaves out several important cases: fragments, portals, booleans used in conditional rendering, null, undefined, and more. Manually enumerating every possibility is brittle and unnecessary.
Instead, it is better to rely on the official React types
The ReactNode Superset Type
ReactNode is the canonical superset type that includes everything React can render. If the objective is to “type children correctly” for a typical component, this is almost always what you want:
children: React.ReactNode;
ReactNode covers:
JSX elements
Plain text and numbers
Arrays of nodes
Fragments
Portals
Conditional render values (null, undefined, false)
In short: if it can appear between JSX tags and React will render it, it fits inside ReactNode.
A simplified version of how ReactNode is defined:
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;
type Props = {
children: ReactNode;
};ReactNode sits at the top of the hierarchy. It includes:
ReactChild – individual renderable units: ReactElement or plain text
ReactFragment – groups of nodes (arrays and fragments)
ReactPortal – elements rendered outside the main DOM tree
boolean | null | undefined – for conditional or empty renders
This makes ReactNode the safest, most ergonomic default for children.
***
How to Use the PropsWithChildren Type
If a component already has its own props and you simply want to add children without writing it manually, React provides React.PropsWithChildren.
PropsWithChildren<P> takes an existing props shape and augments it with an optional children?: ReactNode:
type PropsWithChildren<P> = P & { children?: ReactNode };Example usage:
interface CardProps {
title: string;
}
type CardWithChildrenProps = React.PropsWithChildren<CardProps>;
const Card = ({ title, children }: CardWithChildrenProps) => {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
};
This pattern is particularly useful for library or wrapper components where children is expected but should be optional by default.
How to Use the Component Type for Class Components
While class components are increasingly rare in modern React codebases, they are still occasionally used, especially in legacy projects. For class components, you can rely on React.Component (or the named import Component) to type props, including children.
The important detail: when using Component<P>, the children prop is automatically included as optional:
import { Component } from 'react';
type FooProps = { name: 'foo' };
class Foo extends Component<FooProps> {
render() {
return this.props.children;
}
}Here, FooProps only declares name, yet this.props.children is already available as ReactNode | undefined through Component.
If you want children to be required for a class component, you must explicitly include it in the props type:
type FooProps = {
name: 'foo';
children: React.ReactNode; // now required
};
class Foo extends Component<FooProps> {
render() {
return this.props.children;
}
}
This keeps the behavior consistent with function components, where you also explicitly type children when you want to enforce its presence.
Recent Changes in React 18/19 Regarding children Typing
React.FC / FunctionComponent Is No Longer Recommended
Historically, React.FC (or React.FunctionComponent) was a popular choice for typing function components because it:
Automatically included the children prop
Provided a convenient function-component signature
However, this convenience came with drawbacks:
children was always optional, even when the component conceptually required children.
It encouraged overuse of React.FC even when no children were involved.
It could interfere with certain generic and higher-order component patterns.
In React 18, the @types/react package changed React.FC so it no longer implicitly adds children. Now, if your component accepts children, you must type them explicitly:
interface FooProps {
name: 'foo';
children?: React.ReactNode; // explicitly declare children
}
const Foo = ({ name, children }: FooProps) => {
return <div>{children}</div>;
};
This explicitness is generally preferred in modern React codebases: it makes component APIs clearer, avoids accidental children, and reduces reliance on React.FC altogether.
***
Passing Components as Props in TypeScript (Without React.FC)
You can still pass components as props with full type safety, without ever touching React.FC. For example, consider a to-do form that accepts a button component as a prop:
interface AddTodoProps {
addTodo: (text: string) => void;
ButtonComponent: (props: { onClick: () => void }) => JSX.Element;
}
const AddTodo = ({ addTodo, ButtonComponent }: AddTodoProps) => {
const [text, setText] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!text.trim()) return;
addTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
/>
<ButtonComponent onClick={handleSubmit} />
</form>
);
};
Here:
ButtonComponent is typed as a component that accepts an onClick prop and returns a JSX.Element.
addTodo is strictly defined as a function accepting a string.
You get full type safety on all props, and if at some point ButtonComponent changes its props, TypeScript will immediately surface incompatible usages.
Children, if needed for such components, are now typed explicitly and no longer depend on React.FC’s historical behavior.
***
Refs and forwardRef in React 18 vs React 19
React 18 and Earlier: forwardRef and ComponentPropsWithRef
Before React 19, refs were treated as a special channel separate from props. To write a component that both accepted props (including children) and forwarded a ref, you typically used React.forwardRef and helpers such as ComponentPropsWithRef.
Example:
type ButtonProps = React.ComponentPropsWithRef<'button'>;
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => <button ref={ref}>{props.children}</button>
);ComponentPropsWithRef<'button'> pulls in all the intrinsic props for a button element, plus the appropriate ref typing, including children.
React 19: Typing children and ref Manually
React 19 simplifies the mental model by allowing ref to be treated more like an ordinary prop in many cases. For simple components, you might see typing like this:
type ButtonProps = {
ref?: React.Ref<HTMLButtonElement>;
children?: React.ReactNode;
};
function Button({ ref, children }: ButtonProps) {
return <button ref={ref}>{children}</button>;
}
Here, both children and ref are typed explicitly:
- children?: React.ReactNode for any renderable content
- ref?: React.Ref<HTMLButtonElement> for the forwarded ref
For more complex scenarios or compatibility with older patterns, forwardRef and the React utility types remain available, but the overall direction is toward more explicit, compositional typing.
***
Wrapper Components with TypeScript
Wrapper components — a lightweight form of higher-order components (HOCs) — wrap other components to extend behavior without altering the wrapped component’s implementation. In TypeScript, these wrappers shine by adding strong type safety around props and children.
Consider a simple to-do application. One of the components might look like this:
addTodo: (text: string) => void;
AddTodoProps {
}
const AddTodo = ({ addTodo }: AddTodoProps) => {
// ...
};AddTodo expects an addTodo function that receives a string and returns void. TypeScript guarantees that wherever AddTodo is used, addTodo must conform:
Incorrect prop type:
<AddTodo addTodo={(num: number) => {}} />This will trigger a compile-time error, and your IDE will likely suggest the appropriate fix (changing number to string).
For more complex structures, such as a Todo model shared across multiple components:
export interface Todo {
id: number;
text: string;
completed: boolean;
}When passing a Todo object or arrays of Todo between components, TypeScript ensures all required fields (id, text, completed) are present and correctly typed. This drastically reduces runtime bugs from shape mismatches and makes refactoring safer.
Wrapper components can also use utility types like React.ComponentProps<typeof SomeComponent> or React.ComponentPropsWithoutRef to stay perfectly synchronized with a wrapped component’s props — including its children typing — without duplicating definitions.
Typing the children prop in React becomes straightforward once you align your types with what React can truly render:
ReactNode gives maximum flexibility: anything React can render fits here.
PropsWithChildren elegantly augments existing props with an optional children field.
Component for class components automatically includes optional children, with the option to make them required by extending the props type.
Explicit prop interfaces (rather than React.FC) make component APIs clearer and more predictable, especially in React 18/19.
Wrapper components and components-as-props benefit immensely from strong typing, catching incorrect prop shapes at compile time.
Refs and children are increasingly typed explicitly, aligning the type system with React’s more explicit modern APIs.
Modern React favors explicitness over magic. Instead of leaning on implicit children behavior from React.FC, the recommended approach is to declare children yourself where needed, using ReactNode and React’s utility types. This leads to cleaner, more maintainable, and more robust TypeScript code across both simple app components and complex, reusable component libraries.