Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use React.FC<props> type when the children can either be a React node or a function

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)

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 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.

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?

CodeSandbox link

like image 434
Dimitris Karagiannis Avatar asked Feb 17 '20 18:02

Dimitris Karagiannis


People also ask

How do you use the children prop of a React component?

By invoking them between the opening and closing tags of a JSX element, you can use React children for entering data into a component. The React children prop is an important concept for creating reusable components because it allows components to be constructed together.

What is this props children and when you should use it?

Essentially, props. children is a special prop, automatically passed to every component, that can be used to render the content included between the opening and closing tags when invoking a component. These kinds of components are identified by the official documentation as “boxes”.

How do you pass props in functional component TypeScript?

To pass a function as props in React TypeScript: Define a type for the function property in the component's interface. Define the function in the parent component. Pass the function as a prop to the child component.


2 Answers

tldr:

The React.FC type is the cause for above error:

  1. It already includes default children typed as ReactNode, which get merged (&) with your own children type contained in Props.
  2. 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;
};

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 => ...

Playground sample

like image 196
ford04 Avatar answered Sep 21 '22 10:09

ford04


I think that global union might help:

type Props = {
  children: ((x: number) => ReactNode);
} | {
  children: ReactNode;
};
like image 41
Andrey Avatar answered Sep 20 '22 10:09

Andrey