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

时间:2018-09-26 19:51:30

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

简介

我正在尝试为Higher Order Component中的Dialog组件创建一个React Native。不幸的是,我有一些根本不了解的编译错误。我在Higher Order Components的{​​{1}}上关注了this tutorial,但是没有显示使TypeScript正常工作的示例。

设置

我有一个名为ref的组件,我通过一个名为DialogLoading的{​​{1}}导出了它。 Higher Order Component组件定义了两个接口,以确定它注入了什么道具以及它接受了什么其他道具。在下面的代码中,类型参数withActionswithActionsC分别代表APComponentType

接口为:

ActionType

PropType

我还声明了一个类型别名,该别名表示interface InjectedProps<A> { onActionClicked: (action: A) => void;} 的最终道具类型。此类型应具有包装组件的所有属性,interface ExternalProps<C, A> { onActionClickListener?: (component: C | null, action: A) => void;} 接口的所有属性,而不是HOC接口的所有属性。声明如下:

ExternalProps<C, A>

然后InjectedProps<A>声明如下:

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; } 的render函数内部的ref回调上,<DialogLoading onActionClickListener={this.onActionClickListener} title="Loading Data" section="Connecting" />; 给我以下错误消息:

HOC

我怀疑这是因为传入的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'. ,它是WrappedComponentReact.ComponentType<P>的并集类型。引发错误,因为React中的无状态组件不接受ref回调。然后可能的解决方案是将其类型更改为 just React.ComponentClass<P>

这种种类解决了该问题,但奇怪的是,现在包装组件的React.SFC<P>道具上引发了一个新错误!错误是:

React.ComponentClass<P>

第二个错误使我完全困惑。 甚至陌生人的原因是,当我将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<A>

P上的错误已消除!这对我来说很奇怪,因为type HocProps<C, A, P extends InjectedProps<A>> = P & ExternalProps<C, A>; 的类型定义与包装组件的prop类型无关!但是,这种“解决方案”对我来说是不可取的,因为现在onActionClicked的用户也可以注射HocProps

问题

那么我在哪里错了?

  • 我是否正确假设ref回调不起作用,因为InjectedProps<A>类型是HOC而不是Wrapped Component

  • 为什么将React.ComponentType<P>类型更改为React.ComponentClass<P>会导致Wrapped Component的{​​{1}}道具上出现编译错误?

  • 为什么更改React.ComponentClass<P>的类型别名会消除onActionClicked道具上的错误?他们不是完全无关吗?

  • 我煮熟的Wrapped Component功能正确吗?错误是从哪里来的?

任何帮助将不胜感激,所以在此先感谢!

1 个答案:

答案 0 :(得分:2)

  

我是否正确假设ref回调不起作用,因为Wrapped Component类型是React.ComponentType<P>而不是React.ComponentClass<P>

从广义上讲,是的。当您从WrappedComponent创建一个具有联合类型(React.ComponentType<P> = React.ComponentClass<P> | React.StatelessComponent<P>)的JSX元素时,TypeScript会找到与该联合的每个替代对应的props类型,然后使用子类型来获取props类型的联合减少。来自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<P>的道具类型包括ref,而React.StatelessComponent<P>的道具类型不包括。通常,如果属性存在于联合的至少一个组成部分中,则该属性被视为“已知”。但是,在该示例中,子类型归约排除了React.ComponentClass<P>的props类型,因为它恰好是React.StatelessComponent<P>的props类型的子类型(具有更多的属性),所以我们剩下React.StatelessComponent<P>,而没有ref属性。再次,这一切看起来很奇怪,但是它产生了一个错误,指出了代码中的实际错误,因此我不倾向于针对TypeScript提交错误。

  

为什么将Wrapped Component类型更改为React.ComponentClass<P>会导致onActionClicked的{​​{1}}道具上出现编译错误?

此错误的根本原因是TypeScript无法推断类型Wrapped Component的组合onActionClicked={this.onActionClicked} {...this.props}提供了所需的道具类型Readonly<HocProps<C, A, P>> & { onActionClicked: (action: A) => void; }。您的意图是,如果从P中减去onActionClicked,然后再添加回去,则应该留下P,但是TypeScript没有内置规则可以对此进行验证。 (存在一个潜在的问题,P可以声明类型为P的子类型的onActionClicked属性,但是您的使用模式足够普遍,我希望如果TypeScript要添加这样的属性,一条规则,该规则将以某种方式解决这个问题。)

TypeScript 3.0.3在(action: A) => void上报告错误令人困惑(尽管这可能是由于我提到的问题所致)。我进行了测试,并在3.0.3和3.2.0-dev.20180926之间的某个时刻进行了更改,以报告onActionClicked上的错误,这似乎更合理,因此在此无需进行进一步的跟踪。

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

  

为什么更改P的类型别名会消除HocProps道具上的错误?他们不是完全无关吗?

因为onActionClicked本身就满足要求的{...this.props}

  

我煮熟的P功能正确吗?错误是从哪里来的?

您的Subtract是正确的,但是如上所述,TypeScript对基本SubtractPick的推理几乎没有支持。

要解决您的原始问题,我建议使用类型别名和交集而不是像this answer中所述的减法。就您而言,这看起来像:

Exclude

(这里我还更改了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的赋值进行类型检查。)