How to use React.FC<props> type when the children can either be a React node or a function
Solution 1
tldr:
The React.FC
type is the cause for above error:
- It already includes default
children
typed asReactNode
, which get merged (&
) with your ownchildren
type contained inProps
. -
ReactNode
is a fairly wide type limiting the compiler's ability to narrow down thechildren
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;
};
More details
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.
Solutions
Omit 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
Custom 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 => ...
Solution 2
I think that global union might help:
type Props = {
children: ((x: number) => ReactNode);
} | {
children: ReactNode;
};
Solution 3
Another solution which works, and doesn't require to write the Props
declaration any differently or rewrite anything else differently, is to strictly define the type of the props
parameter, during the component definition, like this
type Props = {
children: ((x: number) => ReactNode) | ReactNode;
};
const Comp: FC<Props> = function Comp(props: Props) { // we strictly define the props type here
...
}
Comp.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired
};
I am not 100% sure why this makes a difference, my intuition is that we "force" our own Props
definition down to the type checker, so we limit the possible scope.
UPDATE
Ever since I asked the original question I eventually settled for the following solution to my problem: I defined my own function component type:
//global.d.ts
declare module 'react' {
// Do not arbitrarily pass children down to props.
// Do not type check actual propTypes because they cannot always map 1:1 with TS types,
// forcing you to go with PropTypes.any very often, in order for the TS compiler
// to shut up
type CFC<P = {}> = CustomFunctionComponent<P>;
interface CustomFunctionComponent<P = {}> {
(props: P, context?: any): ReactElement | null;
propTypes?: { [key: string]: any };
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
}
This solution
- Allows me to strictly define what is a function component
- Does not force any arbitrary
children
prop into my definition - Does not cross reference any actual
Component.propTypes
with the TStype Props = {...}
. Many times they would not map exactly 1:1, and I was forced to usePropTypes.any
which is not what I wanted.
The reason I am keeping the Component.propTypes
along with the TS types, is that while TS is very nice during development, PropTypes
will actually warn in case of a wrong-type value during runtime, which is useful behaviour when, for example, a field in an API response was supposed to be a number and is now a string. Things like this may happen and it's not something TS can help with.
Further Reading
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34237 https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34237#issuecomment-486374424
Dimitris Karagiannis
Working with the Javascripts and the HTMLs and the CSSs. I like music, low level stuff and hardware tinkering.
Updated on June 12, 2022Comments
-
Dimitris Karagiannis almost 2 years
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 asAnything 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 anothernode
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 afunction
)The problem
I am facing the following issues however with the type check.
- if I leave the code as presented here, I get the following error message on the line
? 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.
- if I change the
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 wherechildren
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 astype Props = { children: (x: number) => ReactNode; };
and also change the
Comp.propTypes
to bechildren: PropTypes.func.isRequired
, which is not what I want, since I want to be explicit.The question
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?
-