score:6

Accepted answer

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

score:1

I think that global union might help:

type Props = {
  children: ((x: number) => ReactNode);
} | {
  children: ReactNode;
};

score:1

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 TS type Props = {...}. Many times they would not map exactly 1:1, and I was forced to use PropTypes.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


Related Query

More Query from same tag