Skip to content

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:

tsx
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

The simplest approach is to manually add children to your component props:

tsx
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:

tsx
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:

tsx
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:

tsx
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:

tsx
// types/react.d.ts
import { FC, PropsWithChildren } from 'react';

export type FCC<P = {}> = FC<PropsWithChildren<P>>;

Then use it throughout your project:

tsx
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:

tsx
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

  1. Gradual migration: Use the custom type approach (Method 3) to maintain compatibility while gradually updating components
  2. Codemod tool: Consider using the types-react-codemod for large codebases
  3. Global override (not recommended): As a temporary measure, you can override React types:
tsx
// 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

  1. Be explicit about children requirements: Use children: React.ReactNode for optional children and children: React.ReactNode (without question mark) for required children

  2. Choose consistent patterns: Stick with one approach throughout your project for maintainability

  3. Consider component purpose: Not all components need children - only declare them when necessary

  4. Use appropriate children types:

    • React.ReactNode - most common, accepts any valid React child
    • React.ReactElement - only accepts single React element
    • string - only accepts text content

Common Pitfalls

Incorrect Return Type

Avoid using React.ReactNode as the function return type:

tsx
// ❌ 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.