【问题标题】:React Native - Higher Order Component with ref callback in TypeScript throws compile errorReact Native - TypeScript 中带有 ref 回调的高阶组件引发编译错误
【发布时间】:2019-03-02 16:03:06
【问题描述】:

简介

我正在尝试为React Native 中的Dialog 组件创建一个Higher Order Component。可悲的是,我有一些我根本不理解的编译错误。我在Higher Order Components 和TypeScript 上关注了this tutorial,但它没有显示让ref 工作的示例。

设置

我有一个名为DialogLoading 的组件,我通过一个名为withActions 的Higher Order Component 导出它。 withActionscomponent 定义了两个接口,用于确定它注入了哪些 props 以及它接受了哪些额外的 props。在下面的代码中,类型参数 C、A 和 P 代表 ComponentType、@987654337 @ 和 PropType 分别。

接口是:

interface InjectedProps<A> 
{   onActionClicked: (action: A) => void;}

和

interface ExternalProps<C, A>
{   onActionClickListener?: (component: C | null, action: A) => void;}

我还声明了一个类型别名,表示HOC 的最终道具类型。这个类型应该有被包装组件的所有props,ExternalProps&lt;C, A&gt;接口的所有props,但没有InjectedProps&lt;A&gt;接口的props。声明如下:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Subtract<T, K> = Omit<T, keyof K>;

type HocProps<C, A, P extends InjectedProps<A>> = Subtract<P, InjectedProps<A>> & ExternalProps<C, A>;

Higher Order Component 然后声明如下:

export default <C, A, P extends InjectedProps<A>> (WrappedComponent: React.ComponentType<P>) =>
{
    const hoc = class WithActions extends React.Component<HocProps<C, A, P>>
    {
        ...Contents of class removed for breivity.

        private onActionClicked = (action: A) =>
        {
            this.onActionClickedListeners.forEach(listener => 
            {   listener(this.wrapped, action);});
        }

        private wrapped: C | null;

        render()
        {
            return (
                <WrappedComponent ref={i => this.wrapped = i} onActionClicked={this.onActionClicked} {...this.props} />
            );
        }
    }

    return hoc;
}

并且可以用于:

<DialogLoading onActionClickListener={this.onActionClickListener} title="Loading Data" section="Connecting" />;

问题

在 HOC 的渲染函数内的 ref 回调上,TypeScript 给了我以下错误消息:

[ts] Property 'ref' does not exist on type 'IntrinsicAttributes & InjectedProps<A> & { children?: ReactNode; }'.
[ts] Type 'Component<P, ComponentState, never> | null' is not assignable to type 'C | null'.
     Type 'Component<P, ComponentState, never>' is not assignable to type 'C'.

我怀疑这是因为传入的WrappedComponent 是React.ComponentType&lt;P&gt; 类型,它是React.ComponentClass&lt;P&gt; 和React.SFC&lt;P&gt; 的联合类型。抛出错误,因为 React 中的无状态组件不接受 ref 回调。一个可能的解决方案是将其类型更改为 just React.ComponentClass&lt;P&gt;。

这种种解决了问题,但奇怪的是,包装组件的onActionClicked 属性现在抛出了一个新错误!错误是:

[ts] Type '(action: A) => void' is not assignable to type '(IntrinsicAttributes & IntrinsicClassAttributes<Component<P, ComponentState, never>> & Readonly<{ children?: ReactNode; }> & Readonly<P>)["onActionClicked"]'.
WithActions.tsx(7, 5): The expected type comes from property 'onActionClicked' which is declared here on type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<P, ComponentState, never>> & Readonly<{ children?: ReactNode; }> & Readonly<P>'

第二个错误让我完全困惑。使它更加陌生的是,当我将HocProps 的类型别名调整为以下(即我不再从P 中减去InjectedProps&lt;A&gt;)时:

type HocProps<C, A, P extends InjectedProps<A>> = P & ExternalProps<C, A>;

onActionClicked 的错误已删除!这对我来说似乎很奇怪,因为 HocProps 的类型定义与被包装组件的 prop 类型无关!然而,这种“解决方案”对我来说是不可取的,因为现在InjectedProps&lt;A&gt; 也可以由HOC 的用户注入。

问题

那么我哪里错了?

  • 我是否正确假设 ref 回调不起作用,因为 Wrapped Component 类型是 React.ComponentType&lt;P&gt; 而不是 React.ComponentClass&lt;P&gt;?

  • 为什么将Wrapped Component 类型更改为React.ComponentClass&lt;P&gt; 会导致Wrapped Component 的onActionClicked 属性出现编译错误?

  • 为什么更改HocProps 的类型别名会消除onActionClicked 属性上的错误?他们不是完全没有关系吗?

  • 我编写的Subtract 功能是否正确?这是错误的来源吗?

任何帮助将不胜感激,所以提前致谢!

【问题讨论】:

  • This bug 是问题的一部分。有机会我会多写的。
  • 感谢您的努力,Matt McCutchen!我很想尽快收到您的来信。

标签: javascript reactjs typescript react-native higher-order-components


【解决方案1】:

我是否正确假设 ref 回调不起作用,因为 Wrapped Component 类型是 React.ComponentType&lt;P&gt; 而不是 React.ComponentClass&lt;P&gt;?

粗略地说,是的。当您从具有联合类型 (React.ComponentType&lt;P&gt; = React.ComponentClass&lt;P&gt; | React.StatelessComponent&lt;P&gt;) 的 WrappedComponent 创建 JSX 元素时,TypeScript 会找到与联合的每个替代项对应的道具类型,然后使用子类型归约来获取道具类型的联合。来自checker.ts(太大,无法链接到 GitHub 上的行):

    function resolveCustomJsxElementAttributesType(openingLikeElement: JsxOpeningLikeElement,
        shouldIncludeAllStatelessAttributesType: boolean,
        elementType: Type,
        elementClassType?: Type): Type {

        if (elementType.flags & TypeFlags.Union) {
            const types = (elementType as UnionType).types;
            return getUnionType(types.map(type => {
                return resolveCustomJsxElementAttributesType(openingLikeElement, shouldIncludeAllStatelessAttributesType, type, elementClassType);
            }), UnionReduction.Subtype);
        }

我不知道为什么这是规则;无论组件是哪个联合选项,交集对于确保所有必需的道具都存在更有意义。在我们的示例中,React.ComponentClass&lt;P&gt; 的 props 类型包括 ref,而 React.StatelessComponent&lt;P&gt; 的 props 类型不包括。通常,如果属性存在于联合的至少一个组成部分中,则该属性被认为是联合类型的“已知”。但是,在示例中,子类型缩减会丢弃 React.ComponentClass&lt;P&gt; 的 props 类型,因为它恰好是 React.StatelessComponent&lt;P&gt; 的 props 类型的子类型(具有更多属性),所以我们只剩下 React.StatelessComponent&lt;P&gt; ,它没有 ref 属性。同样,这一切看起来很奇怪,但它产生了一个错误,指出您的代码中存在实际错误,所以我不倾向于针对 TypeScript 提交错误。

为什么将Wrapped Component 类型更改为React.ComponentClass&lt;P&gt; 会导致Wrapped Component 的onActionClicked 属性出现编译错误?

这个错误的根本原因是 TypeScript 无法推断Readonly&lt;HocProps&lt;C, A, P&gt;&gt; &amp; { onActionClicked: (action: A) =&gt; void; } 类型的组合onActionClicked={this.onActionClicked} {...this.props} 提供了所需的道具类型P。您的意图是,如果您从 P 中减去 onActionClicked,然后将其添加回来,则应该留下 P,但 TypeScript 没有内置规则来验证这一点。 (P 可能会声明一个onActionClicked 属性的潜在问题,其类型是(action: A) =&gt; void 的子类型,但是您的使用模式很常见,我预计如果 TypeScript 要添加这样的规则,该规则将以某种方式解决这个问题。)

TypeScript 3.0.3 在onActionClicked 上报告错误令人困惑(尽管这可能是由于我提到的问题)。我测试了,在 3.0.3 和 3.2.0-dev.20180926 之间的某个时间点,行为改变为在WrappedComponent 上报告错误,这似乎更合理,所以这里不需要进一步的跟进。

当WrappedComponent 类型为React.ComponentType&lt;P&gt; 时发生错误不会的原因是因为对于无状态函数组件(与组件类不同),TypeScript 仅检查您是否传递了足够的道具满足道具类型P的约束,即InjectedProps&lt;A&gt;,实际上不是P。我相信这是一个错误并且有reported it。

为什么更改HocProps 的类型别名会消除onActionClicked 属性上的错误?他们不是完全没有关系吗?

因为那么{...this.props} 本身就满足要求的P。

我编写的Subtract 功能是否正确?那是错误的来源吗?

您的Subtract 是正确的,但如上所述,TypeScript 对推理底层Pick 和Exclude 的支持很少。

为了解决您最初的问题,我建议使用类型别名和交集,而不是 this answer 中描述的减法。在您的情况下,这看起来像:

import * as React from "react";

interface InjectedProps<A> 
{   onActionClicked: (action: A) => void;}

interface ExternalProps<C, A>
{   onActionClickListener?: (component: C | null, action: A) => void;}

// See https://stackoverflow.com/a/52528669 for full explanation.
const hocInnerPropsMarker = Symbol();
type HocInnerProps<P, A> = P & {[hocInnerPropsMarker]?: undefined} & InjectedProps<A>;

type HocProps<C, A, P> = P & ExternalProps<C, A>;

const hoc = <C extends React.Component<HocInnerProps<P, A>>, A, P>
    (WrappedComponent: {new(props: HocInnerProps<P, A>, context?: any): C}) =>
{
    const hoc = class WithActions extends React.Component<HocProps<C, A, P>>
    {
        onActionClickedListeners;  // dummy declaration

        private onActionClicked = (action: A) =>
        {
            this.onActionClickedListeners.forEach(listener => 
            {   listener(this.wrapped, action);});
        }

        private wrapped: C | null;

        render()
        {
            // Workaround for https://github.com/Microsoft/TypeScript/issues/27484
            let passthroughProps: Readonly<P> = this.props;
            let innerProps: Readonly<HocInnerProps<P, A>> = Object.assign(
                {} as {[hocInnerPropsMarker]?: undefined},
                passthroughProps, {onActionClicked: this.onActionClicked});
            return (
                <WrappedComponent ref={i => this.wrapped = i} {...innerProps} />
            );
        }
    }

    return hoc;
}

interface DiagLoadingOwnProps {
    title: string;
    section: string;
}

// Comment out the `{[hocInnerPropsMarker]?: undefined} &` in `HocInnerProps`
// and uncomment the following two lines to see the inference mysteriously fail.

//type Oops1<T> = DiagLoadingOwnProps & InjectedProps<string>;
//type Oops2 = Oops1<number>;

class DiagLoadingOrig extends React.Component<
    // I believe that the `A` type parameter is foiling the inference rule that
    // throws out matching constituents from unions or intersections, so we are
    // left to rely on the rule that matches up unions or intersections that are
    // tagged as references to the same type alias.
    HocInnerProps<DiagLoadingOwnProps, string>,
    {}> {}
const DialogLoading = hoc(DiagLoadingOrig);

class OtherComponent extends React.Component<{}, {}> {
    onActionClickListener;
    render() {
        return <DialogLoading onActionClickListener={this.onActionClickListener} title="Loading Data" section="Connecting" />;
    }
}

(这里我还更改了WrappedComponent 的类型,使其实例类型为C,以便对this.wrapped 的赋值进行类型检查。)

【讨论】:

  • 嗨,马特,非常感谢您的详尽解释。您其他答案中的instead of declaring a type variable for the inner props type and defining the outer props type by excluding things, declare a type variable for the outer props type and define the inner props type as an intersection 行已从我的代码中删除了错误。可悲的是,现在 onActionClicked 属性可供 HOC 的最终用户使用。我误解了你的意思吗?如果我们返回ComponentType&lt;P&gt;,P 将始终包含InjectedProps,对吗?无论我们是P extends InjectedProps 还是&amp;。
  • 我不确切知道您尝试了什么,但我继续添加了我的解决方案版本。如果您还有其他问题或疑问,请告诉我。
  • 嗨,马特,再次感谢您的所有努力。在您的示例中,最终用户仍然可以执行以下操作:&lt;DialogLoading onActionClicked={(_: any) =&gt; {}} /&gt;;。 IE。 HOC 的用户仍然可以在最终的 HOC 组件上指定onActionClicked。我仍然不确定如何禁止这样做。
  • 我的代码有几个错误;我以为我测试了它的类型检查但显然没有。对不起。请尝试更新版本。
  • 这似乎工作得很好,谢谢马特!现在是半夜,直到星期三我才有时间正确测试这个。如果到那时一切顺利,我会将您的答案标记为已接受。再次感谢您的辛勤工作!作为一个侧面节点,我不太了解您在解决方案中提到的“脆弱”TypeScript 规则的评论。如果您能对此有所启发,将不胜感激!
猜你喜欢
  • 2017-09-26
  • 1970-01-01
  • 1970-01-01
  • 2019-07-25
  • 1970-01-01
  • 2018-05-28
  • 2019-10-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多