简体   繁体   English

React Native-TypeScript中带有ref回调的高阶组件抛出编译错误

[英]React Native - Higher Order Component with ref callback in TypeScript throws compile error

Intro 介绍

I am trying to create a Higher Order Component for a Dialog component in React Native . 我正在尝试为React NativeDialog组件创建一个Higher Order Component Sadly, I have some compile errors that I do not understand at all. 不幸的是,我有一些根本不了解的编译错误。 I have followed this tutorial on Higher Order Components in TypeScript , but it does not show an example for getting ref to work. 我已经按照TypeScript Higher Order Components 教程进行了学习,但是没有显示使ref正常工作的示例。

Setup 设定

I have a component called DialogLoading , and I export it through a Higher Order Component called withActions . 我有一个名为DialogLoading的组件,并通过一个名为withActions Higher Order Component将其withActions The withActions component defines two interfaces, to determine what props it injects and what additional props it takes in. In the following code, the type parameters C , A and P stand for ComponentType , ActionType and PropType respectively. withActions组件定义了两个接口,以确定其注入的道具以及接受的其他道具。在下面的代码中,类型参数CAP代表ComponentTypeActionTypePropType

The interfaces are: 这些接口是:

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

and

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

I also declare a type alias that denotes the final props type for the HOC . 我还声明了一个类型别名,该别名表示HOC的最终道具类型。 This type should have all the props of the wrapped component, all the props of the ExternalProps<C, A> interface, but not the props of the InjectedProps<A> interface. 此类型应具有包装组件的所有属性, ExternalProps<C, A>接口的所有InjectedProps<A> ,而不应具有InjectedProps<A>接口的所有InjectedProps<A> This is declared as follows: 声明如下:

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

The Higher Order Component is then declared as follows: 然后, 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;
}

and can be used by: 可以由以下人员使用:

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

The Problem 问题

On the ref callback inside the render function of the HOC , TypeScript gives me the following error messages: HOC的render函数内部的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'.

I suspect this is because the WrappedComponent that is passed in, is of type React.ComponentType<P> , which is a union type of React.ComponentClass<P> and React.SFC<P> . 我怀疑这是因为传入的WrappedComponent类型为React.ComponentType<P> ,这是React.ComponentClass<P>React.SFC<P>的联合类型。 The error is thrown, because the stateless components in React do not accept a ref callback. 引发错误,因为React中的无状态组件不接受ref回调。 A possible solution would then be to change it's type to be just React.ComponentClass<P> . 然后可能的解决方案是改变它的类型是 React.ComponentClass<P>

This kind of fixes the problem, but strangely enough, a new error is now thrown on the onActionClicked prop of the wrapped component! 这种方法可以解决问题,但奇怪的是,现在包装组件的onActionClicked道具上引发了一个新错误! The error is: 错误是:

[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>'

This second error baffles me completely. 第二个错误使我完全困惑。 What makes it even stranger is that when I adjust the type alias for the HocProps to the following (ie I no longer Subtract InjectedProps<A> from P ): 令我更加奇怪的是,当我将HocProps的类型别名调整为以下值时(即,我不再从P减去InjectedProps<A> ):

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

The error on onActionClicked is removed! onActionClicked上的错误已删除! This seems strange to me, because the type definition of HocProps has nothing to do with the prop type of the wrapped component! 这对我来说似乎很奇怪,因为HocProps的类型定义与包装组件的prop类型无关! This "solution", however, is undesirable to me, because now the InjectedProps<A> are injectable by the user of the HOC as well. 但是,这种“解决方案”对我来说是不可取的,因为现在InjectedProps<A>也可以由HOC的用户进行InjectedProps<A>

The Question 问题

So where am I going wrong here? 那我在哪里错了?

  • Am I correct in assuming the ref callback did not work because the Wrapped Component type was React.ComponentType<P> instead of React.ComponentClass<P> ? 我是否假设ref回调不起作用是正确的,因为Wrapped Component类型是React.ComponentType<P>而不是React.ComponentClass<P>

  • Why does changing the Wrapped Component type to React.ComponentClass<P> result in a compile error on the onActionClicked prop of the Wrapped Component ? 为什么将Wrapped Component类型更改为React.ComponentClass<P>导致Wrapped ComponentonActionClicked道具出现编译错误?

  • Why does changing the type alias for HocProps remove said error on the onActionClicked prop? 为什么更改HocProps的类型别名HocProps消除onActionClicked道具上的所说错误? Aren't they completely unrelated? 他们不是完全无关吗?

  • Is the Subtract functionality I've cooked up correct? 我煮过的Subtract功能正确吗? Is that where the errors are coming from? 那是错误的来源吗?

Any help would be greatly appreciated, so thanks in advance! 任何帮助将不胜感激,所以在此先感谢!

Am I correct in assuming the ref callback did not work because the Wrapped Component type was React.ComponentType<P> instead of React.ComponentClass<P> ? 我是否假设ref回调不起作用是正确的,因为Wrapped Component类型是React.ComponentType<P>而不是React.ComponentClass<P>

Roughly speaking, yes. 大致来说,是的。 When you make a JSX element from the WrappedComponent , which has a union type ( React.ComponentType<P> = React.ComponentClass<P> | React.StatelessComponent<P> ), TypeScript finds the props type corresponding to each alternative of the union and then takes the union of the props types using subtype reduction. 当从WrappedComponent创建一个具有联合类型的JSX元素( React.ComponentType<P> = React.ComponentClass<P> | React.StatelessComponent<P> )时,TypeScript会找到与联合的每个替代相对应的道具类型然后使用子类型约简获取道具类型的并集。 From checker.ts (which is too big to link to the lines on GitHub): 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);
        }

I'm not sure why this is the rule; 我不知道为什么会这样。 an intersection would make more sense for ensuring that all required props are present regardless of which alternative of the union the component is. 交叉点对于确保所有必需的道具都存在是更有意义的,而与组件的并集的替代方式无关。 In our example, the props type for React.ComponentClass<P> includes ref while the props type for React.StatelessComponent<P> does not. 在我们的例子中,道具键入React.ComponentClass<P>包括ref而道具键入React.StatelessComponent<P>不。 Normally, a property is considered "known" for a union type if it is present in at least one constituent of the union. 通常,如果属性存在于联合的至少一个组成部分中,则该属性被视为“已知”。 However, in the example, the subtype reduction throws out the props type for React.ComponentClass<P> since it happens to be a subtype of (have more properties than) the props type for React.StatelessComponent<P> , so we're left with just React.StatelessComponent<P> , which does not have a ref property. 然而,在该示例中,该子类型减少抛出道具键入React.ComponentClass<P>因为它正好是的一个亚型(具有比多个属性)道具键入React.StatelessComponent<P>所以我们只剩下React.StatelessComponent<P> ,它没有ref属性。 Again, this all seems weird, but it produced an error that pointed to an actual bug in your code, so I'm not inclined to file a bug against TypeScript. 再次,这一切看起来很怪异,但是它产生了一个错误,指出了您的代码中的实际错误,因此我不倾向于针对TypeScript提交错误。

Why does changing the Wrapped Component type to React.ComponentClass<P> result in a compile error on the onActionClicked prop of the Wrapped Component ? 为什么将Wrapped Component类型更改为React.ComponentClass<P>导致Wrapped ComponentonActionClicked道具出现编译错误?

The root cause of this error is that TypeScript is unable to reason that the combination onActionClicked={this.onActionClicked} {...this.props} of type Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; } 发生此错误的根本原因是TypeScript无法推理类型为Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; } onActionClicked={this.onActionClicked} {...this.props} Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; } Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; } provides the required props type P . Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; }提供所需的道具类型P Your intent is that if you subtract onActionClicked from P and then add it back, you should be left with P , but TypeScript has no built-in rule to verify this. 您的意图是,如果从P减去onActionClicked然后再添加回去,则应该保留P ,但是TypeScript没有内置规则可以对此进行验证。 (There is a potential issue that P could declare an onActionClicked property whose type is a subtype of (action: A) => void , but your use pattern is common enough that I expect that if TypeScript were to add such a rule, the rule would hack around this issue somehow.) (存在一个潜在的问题, P可能会声明一个onActionClicked属性,该属性的类型是(action: A) => void的子类型,但是您的使用模式非常普遍,我希望如果TypeScript要添加这样的规则,则该规则会以某种方式解决这个问题。)

It's confusing that TypeScript 3.0.3 reports the error on onActionClicked (though this may be due to the issue I mentioned). TypeScript 3.0.3在onActionClicked上报告错误令人困惑(尽管这可能是由于我提到的问题所致)。 I tested and at some point between 3.0.3 and 3.2.0-dev.20180926, the behavior changed to report the error on WrappedComponent , which seems more reasonable, so no further follow-up is needed here. 我进行了测试,并在3.0.3和3.2.0-dev.20180926之间的某个时刻进行了更改,以报告WrappedComponent上的错误,这似乎更合理,因此在此无需进一步跟进。

The reason the error doesn't occur when the WrappedComponent type is React.ComponentType<P> is because for a stateless function component (unlike a component class), TypeScript only checks that you pass enough props to satisfy the constraint of the props type P , ie, InjectedProps<A> , not actually P . WrappedComponent类型为React.ComponentType<P>发生错误的原因是,对于无状态函数组件(与组件类不同),TypeScript仅检查您传递了足够的道具来满足道具类型P的约束,即InjectedProps<A> ,实际上不是P I believe this is a bug and have reported it . 我相信这是一个错误,并已报告

Why does changing the type alias for HocProps remove said error on the onActionClicked prop? 为什么更改HocProps的类型别名HocProps消除onActionClicked道具上的所说错误? Aren't they completely unrelated? 他们不是完全无关吗?

Because then {...this.props} by itself satisfies the required P . 因为{...this.props}本身就满足要求的P

Is the Subtract functionality I've cooked up correct? 我煮过的Subtract功能正确吗? Is that where the errors are coming from? 那是错误的来源吗?

Your Subtract is correct, but as described above, TypeScript has very little support for reasoning about the underlying Pick and Exclude . 您的Subtract是正确的,但是如上所述,TypeScript对有关基础PickExclude推理几乎没有支持。

To solve your original problem, I recommend using type aliases and intersections instead of subtractions as described in this answer . 为了解决您的原始问题,我建议使用类型别名和交集而不是如本答案中所述的减法。 In your case, this would look like: 您的情况如下所示:

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

(Here I've also changed the type of WrappedComponent so that its instance type is C so that the assignment to this.wrapped type-checks.) (在这里,我还更改了WrappedComponent的类型,以使其实例类型为C以便为此this.wrapped类型检查。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM