当子节点可以是 React 节点或函数时如何使用 React.FC<props> 类型

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

我有这个示例组件

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;

我的意图是组件的 children 属性可以是

这里的要点是明确的,children 可以是一个(node,几乎所有东西)或另一个(只是一个 function)

问题

我在类型检查中遇到了以下问题。

我不明白这个错误。

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

并依赖 React 自己的 type PropsWithChildren<P> = P & { children?: ReactNode }; 来处理 children 不是函数的情况,然后我得到错误

(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

上线children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired

唯一的解决方案是将 Props 类型保留为

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

并将 Comp.propTypes 更改为 children: PropTypes.func.isRequired,这 不是 我想要的,因为我想要明确。

问题

我怎样才能保持代码的明确性,正如这个问题开头所展示的那样,并且不让类型检查抛出错误?

CodeSandbox link

我认为全球联盟可能会有所帮助:

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

另一个可行的解决方案,不需要以任何不同的方式编写 Props 声明或以不同的方式重写任何其他内容,是 strictly 定义props 参数,在组件定义时,像这样

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
};

我不是 100% 确定为什么这会有所不同,我的直觉是我们将自己的 Props 定义“强制”到类型检查器,因此我们限制了可能的范围。

更新

自从我问了最初的问题后,我最终解决了我的问题的以下解决方案:我定义了我自己的函数组件类型

//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;
  }
}

这个解决方案

  • 允许我严格定义什么是函数组件
  • 不强制任何任意 children 道具进入我的定义
  • 不交叉引用任何实际 Component.propTypes 与 TS type Props = {...}。很多时候他们不会准确映射 1:1,我被迫使用 PropTypes.any 这不是我想要的。

我将 Component.propTypes 与 TS 类型一起保留的原因是,虽然 TS 在开发过程中非常好,但 PropTypes 实际上会在运行时出现错误类型值时发出警告,这是有用的行为,例如,API 响应中的一个字段应该是一个数字,现在是一个字符串。这样的事情可能会发生,这不是TS可以帮助的。

进一步阅读

https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34237 https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34237#issuecomment-486374424

tldr:

React.FC type is the cause for above error:

  1. 它已经包含默认 children typed as ReactNode,它与 Props 中包含的您自己的 children 类型合并 (&)。
  2. ReactNode 是一个 fairly wide type 限制编译器将 children 联合类型缩小为可调用函数并结合第 1 点的能力。

一种解决方案是省略 FC 并使用比 ReactNode 更窄的类型以提高类型安全性:

type Renderable = number | string | ReactElement | Renderable[]
type Props = {
  children: ((x: number) => Renderable) | Renderable;
};

更多详情

首先,这里是内置的 React 类型:

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.) 您使用 FC<Props> 键入 CompFC 内部 已经 包含类型为 ReactNodechildren 声明,它与来自 Propschildren 定义合并:

type Props = { children: ((x: number) => ReactNode) | ReactNode } & 
  { children?: ReactNode }
// this is how the actual/effective props rather look like

2.) 查看 ReactNode 类型,您会发现类型变得相当复杂。 ReactNode 通过 ReactFragment 包含类型 {},它是除 nullundefined 之外所有内容的 超类型。我不知道这种类型形状背后的确切决定,microsoft/TypeScript#21699 暗示了历史和向后兼容性的原因。

因此,children 类型比预期的要宽。这会导致您原来的错误:type guard typeof props.children === "function" cannot narrow the type "muddle" properly to function anymore.

解决方案

省略React.FC

最后,React.FC 只是一个具有额外属性的函数类型,如 propTypesdisplayName 等,具有自以为是的宽 children 类型。在此处省略 FC 将为编译器和 IDE 显示带来更安全、更易于理解的类型。如果我采用你对 children 的定义 Anything that can be rendered,那可能是:

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

不带 children

的自定义 FC 类型

您可以定义自己的 FC 版本,其中包含 React.FC 中的所有内容,但那些宽 children 类型除外:

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