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 & {}'"
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"
/>;
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)"
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>>'"
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
};
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 '...'
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:
Previous attempts to add implicit children to Flow:
Further reading
- Pull Request for React 18 types
- [RFC] React 18 and types-only breaking changes
- Prior issues flagging implicit children as incorrect: