当子节点可以是 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
属性可以是
a node
,描述为
Anything that can be rendered: numbers, strings, elements or an array (or fragment) containing these types.
a function
,(或 "render prop"),它只是从组件内部获取一个值,returns 另一个 node
这里的要点是明确的,children
可以是一个(node
,几乎所有东西)或另一个(只是一个 function
)
问题
我在类型检查中遇到了以下问题。
- 如果我保留此处显示的代码,我会在
? 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)
我不明白这个错误。
- 如果我将
Props
类型更改为
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
,这 不是 我想要的,因为我想要明确。
问题
我怎样才能保持代码的明确性,正如这个问题开头所展示的那样,并且不让类型检查抛出错误?
我认为全球联盟可能会有所帮助:
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:
- 它已经包含默认
children
typed as ReactNode
,它与 Props
中包含的您自己的 children
类型合并 (&
)。
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>
键入 Comp
。 FC
内部 已经 包含类型为 ReactNode
的 children
声明,它与来自 Props
的 children
定义合并:
type Props = { children: ((x: number) => ReactNode) | ReactNode } &
{ children?: ReactNode }
// this is how the actual/effective props rather look like
2.) 查看 ReactNode
类型,您会发现类型变得相当复杂。 ReactNode
通过 ReactFragment
包含类型 {}
,它是除 null
和 undefined
之外所有内容的 超类型。我不知道这种类型形状背后的确切决定,microsoft/TypeScript#21699 暗示了历史和向后兼容性的原因。
因此,children
类型比预期的要宽。这会导致您原来的错误:type guard typeof props.children === "function"
cannot narrow the type "muddle" properly to function
anymore.
解决方案
省略React.FC
最后,React.FC
只是一个具有额外属性的函数类型,如 propTypes
、displayName
等,具有自以为是的宽 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 => ...
我有这个示例组件
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
属性可以是
a
node
,描述为Anything that can be rendered: numbers, strings, elements or an array (or fragment) containing these types.
a
function
,(或 "render prop"),它只是从组件内部获取一个值,returns 另一个node
这里的要点是明确的,children
可以是一个(node
,几乎所有东西)或另一个(只是一个 function
)
问题
我在类型检查中遇到了以下问题。
- 如果我保留此处显示的代码,我会在
? 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)
我不明白这个错误。
- 如果我将
Props
类型更改为
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
,这 不是 我想要的,因为我想要明确。
问题
我怎样才能保持代码的明确性,正如这个问题开头所展示的那样,并且不让类型检查抛出错误?
我认为全球联盟可能会有所帮助:
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
与 TStype 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:
- 它已经包含默认
children
typed asReactNode
,它与Props
中包含的您自己的children
类型合并 (&
)。 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>
键入 Comp
。 FC
内部 已经 包含类型为 ReactNode
的 children
声明,它与来自 Props
的 children
定义合并:
type Props = { children: ((x: number) => ReactNode) | ReactNode } &
{ children?: ReactNode }
// this is how the actual/effective props rather look like
2.) 查看 ReactNode
类型,您会发现类型变得相当复杂。 ReactNode
通过 ReactFragment
包含类型 {}
,它是除 null
和 undefined
之外所有内容的 超类型。我不知道这种类型形状背后的确切决定,microsoft/TypeScript#21699 暗示了历史和向后兼容性的原因。
因此,children
类型比预期的要宽。这会导致您原来的错误:type guard typeof props.children === "function"
cannot narrow the type "muddle" properly to function
anymore.
解决方案
省略React.FC
最后,React.FC
只是一个具有额外属性的函数类型,如 propTypes
、displayName
等,具有自以为是的宽 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 => ...