React 18 TypeScript: Handling Children Props in Functional Components
Problem Statement
With the release of React 18, many developers encountered TypeScript errors when using functional components with children. The error message typically appears as:
Property 'children' does not exist on type 'IPageProps'
This occurs because React 18's TypeScript definitions removed the implicit children
prop from the React.FC
(FunctionComponent) type. Previously, React 17 automatically included children in the component props, but this behavior has changed to enforce more explicit type definitions.
Why This Change Was Made
The React team made this change to prevent common bugs where components that weren't designed to accept children would still technically allow them. For example:
const ComponentWithNoChildren: React.FC = () => <>Hello</>;
// This would compile in React 17 but is logically incorrect
<ComponentWithNoChildren>
<UnusedChildrenSinceComponentHasNoChildren />
</ComponentWithNoChildren>
By requiring explicit declaration of children props, React 18 encourages more intentional component design and prevents accidental passing of children to components that don't use them.
Solutions
Method 1: Manual Children Declaration (Recommended)
The simplest approach is to manually add children to your component props:
import React from 'react';
interface Props {
children?: React.ReactNode;
// other props...
}
const Component: React.FC<Props> = ({ children }) => {
return <div>{children}</div>;
};
For components that require children:
interface Props {
children: React.ReactNode; // No question mark = required
// other props...
}
Method 2: Using React's PropsWithChildren Utility
React provides a built-in type helper for components with children:
import React, { PropsWithChildren } from 'react';
interface Props {
// your other props...
}
const Component: React.FC<PropsWithChildren<Props>> = ({ children, ...props }) => {
return <div {...props}>{children}</div>;
};
You can also extend your interface:
import React from 'react';
interface Props extends React.PropsWithChildren {
// your other props...
}
const Component: React.FC<Props> = ({ children, ...props }) => {
return <div {...props}>{children}</div>;
};
Method 3: Create a Custom FC Type
For projects with many components, create a custom type:
// types/react.d.ts
import { FC, PropsWithChildren } from 'react';
export type FCC<P = {}> = FC<PropsWithChildren<P>>;
Then use it throughout your project:
import { FCC } from '../types/react';
interface Props {
// your props...
}
const Component: FCC<Props> = ({ children, ...props }) => {
return <div {...props}>{children}</div>;
};
Method 4: Avoid React.FC Altogether
Many developers prefer skipping React.FC
entirely:
import React from 'react';
interface Props {
children?: React.ReactNode;
// other props...
}
function Component({ children, ...props }: Props) {
return <div {...props}>{children}</div>;
}
TIP
When not using React.FC
, you don't need to specify a return type since TypeScript can infer it from the JSX return.
Migration Strategies
For New Projects
Start with explicit children declaration from the beginning using either Method 1 or Method 4.
For Existing Projects
- Gradual migration: Use the custom type approach (Method 3) to maintain compatibility while gradually updating components
- Codemod tool: Consider using the types-react-codemod for large codebases
- Global override (not recommended): As a temporary measure, you can override React types:
// react.d.ts
import * as React from '@types/react';
declare module 'react' {
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
}
}
WARNING
The global override approach reverts to React 17 behavior and eliminates the benefits of the stricter typing. Use only as a last resort.
Best Practices
Be explicit about children requirements: Use
children: React.ReactNode
for optional children andchildren: React.ReactNode
(without question mark) for required childrenChoose consistent patterns: Stick with one approach throughout your project for maintainability
Consider component purpose: Not all components need children - only declare them when necessary
Use appropriate children types:
React.ReactNode
- most common, accepts any valid React childReact.ReactElement
- only accepts single React elementstring
- only accepts text content
Common Pitfalls
Incorrect Return Type
Avoid using React.ReactNode
as the function return type:
// ❌ Problematic
function Component(): React.ReactNode {
return <div>Hello</div>;
}
// ✅ Correct
function Component() {
return <div>Hello</div>;
}
Using React.ReactNode
as return type can cause TypeScript errors when the component is used in JSX.
Conclusion
The React 18 TypeScript changes require developers to explicitly declare children props, which leads to more robust and intentional component design. While this change requires updates to existing code, the long-term benefits include better type safety and clearer component contracts.
Choose the approach that best fits your project's needs:
- Manual declaration for simplicity
PropsWithChildren
for React's built-in solution- Custom FCC type for large codebases
- Avoiding
React.FC
for minimalism
By embracing these changes, you'll create more maintainable React applications with proper TypeScript support.