Skip to main content (Press Enter)

Removal of implicit children

Explainer why we intend to get rid of implicit children in `@types/react`

With React 18 we have the opportunity in @types/react to fix long-standing issues we had to deal with for a long time. We originally wanted to fix these in React 17, but held off because React 17 was a big step in enabling gradual migration. One of these changes is removal of implicit children in React.FunctionComponent types. I’ll try to explain why we want to make this change and how you can ease migration.

Just show me what breaks!

import * as React from 'react';

const Input: React.FC = ({ children }) => <div>{children}</div>;
//                         ^^^^^^^^ will error with "Property 'children'
//                                  does not exist on type '{}'.
<Input>children</Input>;
//^^^^ will error with "Type '{ children: string; }' has no properties in common
//                      with type 'IntrinsicAttributes & {}'"

TypeScript Playground

What are implicit children?

We differentiate implicit and explicit props for declaration of components. Explicit props are the ones that are written out in the props interface. Implicit props are the ones that @types/react automatically adds.

import * as React from 'react';

interface InputProps {
	type: string;
}

const Input: React.FC<InputProps> = ({ type }) => {
	return <input type={type} />;
};

const ref = React.createRef();
<Input
	// implicit props
	key="first"
	// explicit props
	type="search"
/>;

TypeScript Playground

ref is also an implicit prop if the component does implement it (e.g. class components React.Component or ref-forwarding components React.forwardRef).

In @types/react@^17.0.0 and below children was also an implicit prop.

Problems

While implicit children are definitely nice to have to quickly type out a component, they’ll also hide a variety of bugs.

Excess props are generally rejected

Excess props are props that are passed to a component, but not actually handled in a component. This catches typos in prop names or wrong assumption about the effect a prop might have:

import * as React from 'react';

interface InputProps {
	type?: string;
}

const Input = ({ type }: InputProps) => {
	return <input type={type} />;
};

<Input type="search" inputMode="numeric" />;
//                   ^^^^^^^^^ excess prop that's rejected
<Input typ="search" />;
//     ^^^ "Type '{ typ: string; }' is not assignable to type
//          'IntrinsicAttributes & InputProps'.
//             Property 'typ' does not exist on type
//             'IntrinsicAttributes & InputProps'. Did you mean 'type'?(2322)"

TypeScript Playground

children is just another excess prop in this case. This is surprising behavior which should be avoided in general. What’s even worse is that this excess prop is caught when you’re not using React.FC.

In summary, removing implicit children:

  • results in consistent behavior between React.FC and simple function declarations.
  • catches excess children props

Better errors for narrower children

If your children are a subset of the types allowed by ReactNode you no longer have to guess what types are accepted by MyChildrenType & ReactNode:

import * as React from 'react';

interface InputProps {
	children: React.ReactElement;
}

const Input: React.FC<InputProps> = ({ children }) => {
	return React.cloneElement(children);
};

<Input>foo</Input>;
//     ^^^ "'Input' components don't accept text as child elements. Text in JSX
//           has the type 'string', but the expected type of 'children' is
//           'ReactElement<any, string | JSXElementConstructor<any>> & ReactNode'"
// No more thinking about the intersection results in. It'll just error with
// "'Input' components don't accept text as child elements. Text in JSX has the type
//  'string', but the expected type of 'children' is
//  'ReactElement<any, string | JSXElementConstructor<any>>'"

TypeScript Playground

Allows removal of {} from ReactNode

Why remove {} from ReactNode?

It allows passing of objects, functions etc .to host components which errors at runtime. A previous attempt to correct ReactNode revealed that {} was required due to implicit children.

DefinitelyTyped/DefinitelyTyped#56026 has multiple linked issues that contain fixes that were hidden by {} being included in ReactNode (e.g. DefinitelyTyped/DefinitelyTyped#56035).

Implicit children when {} is not part of ReactNode

Due to implicit children, narrowing children results in children being typed as NarrowerChildrenType & React.ReactNode.

string acts here as our NarrowerChildrenType

import * as React from 'react';

interface RenderProps {
	type: string;
}

interface InputProps {
	children: string;
}

const Input: React.FC<InputProps> = ({ children }) => {
	return <div>{children}</div>;
	//           ^^^^^^^^ (parameter) children: string & React.ReactNode
};

TypeScript Playground

This isn’t a problem if your NarrowerChildrenType is a subtype of ReactNode. However, once you have a specific object or function type, you’ll start seeing type errors:

import * as React from 'react';

type ReactNodeWithoutObject =
	| React.ReactElement
	| string
	| number
	| boolean
	| null
	| undefined;

interface RenderProps {
	type: string;
}

interface InputProps {
	children: (props: RenderProps) => React.ReactElement;
}

const Input = ({
	children,
}: InputProps & { children?: ReactNodeWithoutObject }) => {
	return children({ type: 'search' });
};

<Input>{({ type }) => <input type="search" />}</Input>;
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "'...' is not assignable to '...'

TypeScript Playground

Sync with Flow types

While you may consider Flow as “dead language” it’s still used to type the React codebase itself. Flow types do not add implicit children:

  1. facebook/react definition of function components

  2. facebook/flow definition of function components

Previous attempts to add implicit children to Flow:

Further reading


Webmentions

Failed to load...