I have this sample component
import React, { FC, ReactNode, useMemo } from "react";
import PropTypes from "prop-types";
type Props = {
children: ((x: number) => ReactNode) | ReactNode;
};
const Comp: FC<Props> = function Comp(props) {
const val = useMemo(() => {
return 1;
}, []);
return (
<div>
{typeof props.children === "function"
? props.children(val)
: props.children}
</div>
);
};
Comp.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired
};
export default Comp;
My intent here is that the children
prop of the component can either be
a node
, which is described as
Anything that can be rendered: numbers, strings, elements or an array (or fragment) containing these types.
a function
, (or a "render prop") which simply gets a value from inside the component and returns another node
the point here is to be explicit, that the children
can either be the one (node
, which is pretty much everything) or the other (which is simply a function
)
I am facing the following issues however with the type check.
? props.children(val)
This expression is not callable. Not all constituents of type 'Function | ((x: number) => ReactNode) | (string & {}) | (number & {}) | (false & {}) | (true & {}) | ({} & string) | ({} & number) | ({} & false) | ({} & true) | (((x: number) => ReactNode) & string)
I do not understand this error.
Props
type to be type Props = {
children: (x: number) => ReactNode;
};
and rely on React's own type PropsWithChildren<P> = P & { children?: ReactNode };
to handle the case where children
is not a function, then I get the error
(property) children?: PropTypes.Validator<(x: number) => React.ReactNode> Type 'Validator' is not assignable to type 'Validator<(x: number) => ReactNode>'. Type 'ReactNodeLike' is not assignable to type '(x: number) => ReactNode'. Type 'string' is not assignable to type '(x: number) => ReactNode'.ts(2322) Comp.tsx(5, 3): The expected type comes from property 'children' which is declared here on type
on the line children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired
The only solution is to leave the Props
type as
type Props = {
children: (x: number) => ReactNode;
};
and also change the Comp.propTypes
to be children: PropTypes.func.isRequired
, which is not what I want, since I want to be explicit.
How can I keep the code explicit, as presented at the start of this question, and also not have the type checking throw errors on me?
The React.FC
type is the cause for above error:
children
typed as ReactNode
, which get merged (&
) with your own children
type contained in Props
.ReactNode
is a fairly wide type limiting the compiler's ability to narrow down the children
union type to a callable function in combination with point 1.A solution is to omit FC
and use a more narrow type than ReactNode
to benefit type safety:
type Renderable = number | string | ReactElement | Renderable[]
type Props = {
children: ((x: number) => Renderable) | Renderable;
};
First of all, here are the built-in React types:
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean
| null | undefined;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
type PropsWithChildren<P> = P & { children?: ReactNode };
1.) You use FC<Props>
to type Comp
. FC
internally already includes a children
declaration typed as ReactNode
, which gets merged with children
definition from Props
:
type Props = { children: ((x: number) => ReactNode) | ReactNode } &
{ children?: ReactNode }
// this is how the actual/effective props rather look like
2.) Looking at ReactNode
type, you'll see that types get considerably more complex. ReactNode
includes type {}
via ReactFragment
, which is the supertype of everything except null
and undefined
. I don't know the exact decisions behind this type shape, microsoft/TypeScript#21699 hints at historical and backward-compatiblity reasons.
As a consequence, children
types are wider than intended. This causes your original errors: type guard typeof props.children === "function"
cannot narrow the type "muddle" properly to function
anymore.
React.FC
In the end, React.FC
is just a function type with extra properties like propTypes
, displayName
etc. with opinionated, wide children
type. Omitting FC
here will result in safer, more understandable types for compiler and IDE display. If I take your definition Anything that can be rendered
for children
, that could be:
import React, { ReactChild } from "react";
// You could keep `ReactNode`, though we can do better with more narrow types
type Renderable = ReactChild | Renderable[]
type Props = {
children: ((x: number) => Renderable) | Renderable;
};
const Comp = (props: Props) => {...} // leave out `FC` type
FC
type without children
You could define your own FC
version, that contains everything from React.FC
except those wide children
types:
type FC_NoChildren<P = {}> = { [K in keyof FC<P>]: FC<P>[K] } & // propTypes etc.
{ (props: P, context?: any): ReactElement | null } // changed call signature
const Comp: FC_NoChildren<Props> = props => ...