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

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

简介

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

设置

我有一个名为 DialogLoading 的组件,我通过一个名为 withActionsHigher Order Component 导出它。 withActions 组件定义了两个接口,以确定它注入的 props 和接收的额外 props。在下面的代码中,类型参数 CAP分别代表 ComponentTypeActionTypePropType

接口是:

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

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

我还声明了一个类型别名,表示 HOC 的最终道具类型。这种类型应该具有包装组件的所有 props,ExternalProps<C, A> 接口的所有 props,但没有 InjectedProps<A> 接口的 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'.

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

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

[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> ):

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

onActionClicked 上的错误已删除!这对我来说似乎很奇怪,因为 HocProps 的类型定义与包装组件的 prop 类型无关!然而,这个 "solution" 对我来说是不受欢迎的,因为现在 InjectedProps<A> 也可以被 HOC 的用户注入。

问题

那么我哪里错了?

如有任何帮助,我们将不胜感激,在此先致谢!

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

粗略地说,是的。当你从 WrappedComponent 创建一个 JSX 元素时,它有一个联合类型(React.ComponentType<P> = React.ComponentClass<P> | React.StatelessComponent<P>),TypeScript 找到对应于联合的每个备选方案的道具类型,然后使用子类型获取道具类型的联合减少。从 checker.ts(太大 link 到 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);
        }

我不确定为什么这是规则;交集对于确保所有必需的 props 都存在更有意义,而不管组件是哪个 union 的替代方案。在我们的示例中,React.ComponentClass<P> 的道具类型包括 ref,而 React.StatelessComponent<P> 的道具类型不包括。通常,如果 属性 出现在联合的至少一个组成部分中,则它被认为是联合类型的 "known"。但是,在示例中,子类型缩减会抛出 React.ComponentClass<P> 的道具类型,因为它恰好是 React.StatelessComponent<P> 的道具类型的子类型(具有更多属性),所以我们剩下只有 React.StatelessComponent<P>,没有 ref 属性。同样,这一切看起来很奇怪,但它产生了一个错误,指向您代码中的实际错误,因此我不倾向于针对 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?

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

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

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

Why does changing the type alias for HocProps remove said error on the onActionClicked prop? Aren't they completely unrelated?

因为 {...this.props} 本身就满足 P.

Is the Subtract functionality I've cooked up correct? Is that where the errors are coming from?

您的 Subtract 是正确的,但如上所述,TypeScript 对基础 PickExclude.

的推理支持很少

为了解决您原来的问题,我建议使用类型别名和交集而不是 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  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 进行类型检查。)