Keywords: React 18 | TypeScript | FC Interface | Children Type | PropsWithChildren
Abstract: This article explores the significant change in React 18 where the FC interface no longer implicitly includes the children property in TypeScript. By analyzing the official update motivations, comparing old and new code patterns, it details three solutions: manually defining children types, using the PropsWithChildren helper type, and abandoning FC altogether. With concrete code examples, it explains the correct usage of React.ReactNode as the standard type for children and offers balanced advice on type safety and development efficiency to help developers smoothly transition to React 18's type system.
With the release of React 18, TypeScript developers face a notable change in the type system: the React.FC (FunctionComponent) interface no longer automatically includes the children property. This change stems from React team's efforts to enhance type safety, aiming to eliminate potential bugs caused by implicit children passing. In React 17 and earlier, the FC type implicitly added children?: React.ReactNode, allowing developers to use children in components without explicit declaration. However, this design permitted passing children to components that should not receive them, violating the component's intended contract.
Background and Motivation
React 18, through the @types/react 18.0.0 update, removed the implicit children definition from FC. The core of this decision lies in improving the precision of the type system. Consider a scenario: a pure presentation component that only renders static content should not accept any children. Under the old type system, TypeScript would not error even if the component did not define children, leading to runtime children being ignored without easy detection. The new type system requires explicit declaration, catching such logical errors at compile time.
Solution 1: Manually Define Children Type
The most direct approach is to explicitly add the children property in the component's props interface. It is recommended to use React.ReactNode as the type, as it covers all possible forms of children, including strings, numbers, elements, fragments, and more. Here is a complete example:
import * as React from 'react';
type ComponentProps = {
title: string;
children?: React.ReactNode;
};
const MyComponent: React.FC<ComponentProps> = ({ title, children }) => {
return (
<div>
<h1>{title}</h1>
{children}
</div>
);
};
This method maintains the habit of using FC, requiring only one additional line in the type definition. Note that children is set as an optional property (?) to accommodate cases where no children are passed, aligning with common React patterns.
Solution 2: Utilize PropsWithChildren Helper Type
React provides the React.PropsWithChildren generic utility, which simplifies type composition. It accepts a props type parameter and automatically adds children?: React.ReactNode. Usage is as follows:
import * as React from 'react';
type ComponentProps = {
title: string;
};
const MyComponent: React.FC<React.PropsWithChildren<ComponentProps>> = ({ title, children }) => {
return (
<div>
<h1>{title}</h1>
{children}
</div>
);
};
Or achieve it through interface inheritance:
interface ComponentProps extends React.PropsWithChildren {
title: string;
}
This approach reduces code duplication, especially beneficial for large projects. If React adjusts the children type in the future, PropsWithChildren will update automatically, ensuring type consistency.
Solution 3: Abandon FC and Use Function Declaration
Another trend is to avoid FC and declare components directly as functions. This eliminates dependency on the FC type, making code cleaner and easier to understand. Example:
import * as React from 'react';
type ComponentProps = {
title: string;
children?: React.ReactNode;
};
function MyComponent({ title, children }: ComponentProps): React.ReactElement {
return (
<div>
<h1>{title}</h1>
{children}
</div>
);
}
This method explicitly sets the component's return type as React.ReactElement, enhancing type transparency. It encourages developers to think more carefully about props structure, aligning with React's philosophy of treating children as a regular prop.
Type Selection and Best Practices
For the type of children, React.ReactNode is the most general choice, but more specific types like React.ReactElement or string can be used based on context. When migrating existing projects, it is advisable to update component types gradually rather than globally overriding them. Codemod tools can automate part of the conversion, but manual review ensures logical correctness. Always enable strict mode ("strict": true) in tsconfig.json to fully leverage TypeScript's type-checking capabilities.
Common Errors and Debugging
Common TypeScript errors after upgrading include: Property 'children' does not exist on type '...' or Type '{ children: ...; }' has no properties in common with type 'IntrinsicAttributes'. These indicate that children is not defined in the props type. The solution is to check the component type definition and ensure it includes a children declaration. For third-party library components, consult their documentation to confirm if children are supported.
In summary, the type change in React 18 is a significant step towards a safer and more explicit design. By handling children explicitly, developers can build more reliable applications and reduce runtime errors. Choosing any of the above solutions and adapting them to project needs will enable an efficient transition to this change.