正确的Typescript类型可用于React中嵌套的prop-jection-HOCs返回值

时间:2018-09-26 14:30:48

标签: reactjs typescript

我想知道在以下情况下我是否具有正确的API HOC返回类型类型:

  • 我有一个身份验证HOC,withAuthentication,它将身份验证服务注入到组件的props中。

  • 我有一个API HOC,withRestApi,它会注入API调用,它本身使用withAuthentication

  • MyComponent需要利用withRestApi的注入函数,并具有自己的道具。

(在实际应用中,withAuthentication还需要一个反应路由器HOC,但在此示例中,我们试图使事情尽可能简单。)

到目前为止,我的代码都是基于James Ravencroft's excellent post on HOCs and Typescript以及this SO post on injected HOC props的,这有助于解决HOC道具暴露给包装组件的父对象的问题。

我想要实现的是:

  • this.props.getResultOfApiCall类中访问MyComponent,但对任何父组件都隐藏。
  • this.props.isAuthenticated类中访问WithRestApi,但对任何父组件都隐藏。
  • 能够通过父组件在componentProp上设置MyComponent

代码如下:

MyBase.tsx ,仅包含用于演示MyComponent道具的使用的组件:

import * as React from 'react';

import MyComponent from './MyComponent';

class MyBase extends React.Component {
    public render() {
        return (
            <>
                <h1>RESULT</h1>
                <MyComponent componentProp={'Prop belonging to MyComponent'} />
            </>
        );
    }
}

export default MyBase;

MyComponent.tsx ,它使用以下API:

import * as React from 'react';

import { IWithRestApiProps, withRestApi } from './WithRestApi';

interface IMyComponentProps extends IWithRestApiProps {
    componentProp: string;
}

class MyComponent extends React.Component<IMyComponentProps> {
    public render() {
        return (
            <>
                <h2>Component prop: {this.props.componentProp}</h2>
                <h2>API result: {this.props.getResultOfApiCall()}</h2>
            </>
        );
    }
}

export default withRestApi(MyComponent);

WithAuthentication.tsx (首先讲这是因为这不是问题,据我所知):

import * as React from 'react';

export interface IWithAuthenticationProps {
    isAuthenticated: () => boolean;
}

export const withAuthentication = <P extends IWithAuthenticationProps>(Component: React.ComponentType<P>):
    React.ComponentType<Pick<P, Exclude<keyof P, keyof IWithAuthenticationProps>>> =>
    class WithAuthentication extends React.Component<P> {
        public render() {

            const { isAuthenticated, ...originalProps } = this.props as IWithAuthenticationProps; 

            return (
                <Component
                    {...originalProps}
                    isAuthenticated={this.isAuthenticated}
                />
            );
        }

        private readonly isAuthenticated = (): boolean => {
            return true;
        }
    }

WithRestApi.tsx ,其中包含键入问题。

import * as React from 'react';

import { IWithAuthenticationProps, withAuthentication } from './WithAuthentication';

export interface IWithRestApiProps extends IWithAuthenticationProps {
    getResultOfApiCall: () => string;
}

export const withRestApi = <P extends IWithRestApiProps>(Component: React.ComponentType<P>):
    React.ComponentType<Pick<P, Exclude<keyof P, keyof IWithRestApiProps>>> =>
    withAuthentication(class WithRestApi extends React.Component<P> {
        public render() {

            const { getResultOfApiCall, ...originalProps } = this.props as IWithRestApiProps; 

            return (
                <Component
                    {...originalProps}
                    getResultOfApiCall={this.getApiData}
                />
            );
        }

        private readonly getApiData = () => {
            if (this.props.isAuthenticated()) {
                return 'Some API result';
            } else {
                return 'Not authenticated';
            }
        }
    }) as React.ComponentType<P>; // TODO - remove this type assertion...?

此代码已构建,但是如您所见,我不得不将withApi HOC的返回值类型声明为React.ComponentType<P>。没有该断言,我会看到此Typescript错误:

[ts]
Type 'ComponentType<Pick<P, Exclude<keyof P, "isAuthenticated">>>' is not assignable to type 'ComponentType<Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>>'.
  Type 'ComponentClass<Pick<P, Exclude<keyof P, "isAuthenticated">>, ComponentState>' is not assignable to type 'ComponentType<Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>>'.
    Type 'ComponentClass<Pick<P, Exclude<keyof P, "isAuthenticated">>, ComponentState>' is not assignable to type 'ComponentClass<Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>, ComponentState>'.
      Type 'Pick<P, Exclude<keyof P, "isAuthenticated">>' is not assignable to type 'Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>'.
        Type 'Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">' is not assignable to type 'Exclude<keyof P, "isAuthenticated">'.

这是我第一次遇到对Pick..Exclude的需求,因此在这种情况下确切地如何匹配类型还有些模糊。我想知道我在这里使用它的方式是否可以改进以消除对类型断言的需要?

1 个答案:

答案 0 :(得分:1)

第一个问题是,HOC中的组件类定义不正确,因为您将看到是否启用了strictFunctionTypes编译器选项。从外部到WithAuthentication的道具不包括isAuthenticatedWithAuthentication会自行生成该道具。因此WithAuthentication的道具类型应排除IWithAuthenticationProps

// ...
class WithAuthentication extends React.Component<Pick<P, Exclude<keyof P, keyof IWithAuthenticationProps>>> {
    public render() {
        const originalProps = this.props;
        // ...
    }
}
// ...

对于WithRestApi同样(并删除类型断言):

// ...
withAuthentication(class WithRestApi extends React.Component<Pick<P, Exclude<keyof P, keyof IWithRestApiProps>> & IWithAuthenticationProps> {
    public render() {
        const originalProps = this.props;
        // ...
    }
    // ...
}
// ...

现在,您因为TypeScript无法简化PickExcludeWithRestApi.tsx类型的复杂组合而感到痛苦。错误(我漂亮地打印出来)是:

Type 'ComponentType<
  Pick<
    Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>
      & IWithAuthenticationProps,
    Exclude<Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">, "isAuthenticated">
  >
>'
is not assignable to type
'ComponentType<Pick<P, Exclude<keyof P, "getResultOfApiCall" | "isAuthenticated">>>'.

我们可以查看此错误消息,并意识到这两种类型对于P的任何选择都是等效的,但是TypeScript没有必要的代数规则来证明这一点。

所以我建议采用另一种方法。对于每个HOC,不是为内部道具类型声明类型变量并通过排除事物来定义外部道具类型,而是为外部道具类型声明类型变量并将内部道具类型定义为交集。 TypeScript在简化和执行涉及交点的推理方面要好得多。缺点是MyComponent的props类型必须写为匹配形式的交集,而不仅仅是接口。您可以定义类型别名以生成必要的交集。解决方案:

WithAuthentication.tsx

import * as React from 'react';

// The `{[withAuthenticationPropsMarker]?: undefined}` constituent ensures that
// `WithAuthenticationProps<{}>` is still an intersection so that the inference
// rule that throws out matching constituents between
// `WithAuthenticationProps<{}>` and `WithAuthenticationProps<OrigProps>` still
// works.  In case that rule isn't applicable, the checker tags each union or
// intersection type with the first type alias reference it sees that produces
// the union or intersection type, and there's an inference rule that matches up
// the type arguments of union or intersection types produced by instantiating
// the same type alias.  Normally this is fragile because it depends on the
// desired type alias being the first one seen in the compilation, but our use
// of a unique marker should ensure that nothing else can produce and tag the
// intersection type before we do.
const withAuthenticationPropsMarker = Symbol();
export type WithAuthenticationProps<OrigProps> = OrigProps &
    {[withAuthenticationPropsMarker]?: undefined} & {
    isAuthenticated: () => boolean;
};

export const withAuthentication = <P extends {}>(Component: React.ComponentType<WithAuthenticationProps<P>>):
    React.ComponentType<P> =>
    class WithAuthentication extends React.Component<P> {
        public render() {
            return (
                <Component
                    {...this.props}
                    isAuthenticated={this.isAuthenticated}
                />
            );
        }

        private readonly isAuthenticated = (): boolean => {
            return true;
        }
    }

WithRestApi.tsx

import * as React from 'react';

import { WithAuthenticationProps, withAuthentication } from './WithAuthentication';

const withRestApiPropsMarker = Symbol();
export type WithRestApiProps<OrigProps> = OrigProps &
    {[withRestApiPropsMarker]?: undefined} & {
    getResultOfApiCall: () => string;
}

export const withRestApi = <P extends {}>(Component: React.ComponentType<WithRestApiProps<P>>):
    React.ComponentType<P> =>
    withAuthentication(class WithRestApi extends React.Component<WithAuthenticationProps<P>> {
        public render() {
            // @ts-ignore : "Rest types may only be created from object types"
            // https://github.com/Microsoft/TypeScript/issues/10727
            let {isAuthenticated, ...otherPropsUntyped} = this.props;
            let otherProps: P = otherPropsUntyped;

            return (
                <Component
                    {...otherProps}
                    getResultOfApiCall={this.getApiData}
                />
            );
        }

        private readonly getApiData = () => {
            if (this.props.isAuthenticated()) {
                return 'Some API result';
            } else {
                return 'Not authenticated';
            }
        }
    });

MyComponent.tsx

import * as React from 'react';

import { WithRestApiProps, withRestApi } from './WithRestApi';

type MyComponentProps = WithRestApiProps<{
    componentProp: string;
}>;

class MyComponent extends React.Component<MyComponentProps> {
    public render() {
        return (
            <>
                <h2>Component prop: {this.props.componentProp}</h2>
                <h2>API result: {this.props.getResultOfApiCall()}</h2>
            </>
        );
    }
}

export default withRestApi(MyComponent);

跟进:从isAuthenticated包装的组件中隐藏withRestApi

将道具隐藏在类型中是将WithRestApiProps的定义更改为:

export type WithRestApiProps<OrigProps> = OrigProps & {
    getResultOfApiCall: () => string;
}

该道具仍将在运行时传递。如果要避免这种情况,可以将WithRestApi.render更改为:

    public render() {
        // @ts-ignore : "Rest types may only be created from object types"
        // https://github.com/Microsoft/TypeScript/issues/10727
        let {isAuthenticated, ...otherPropsUntyped} = this.props;
        let otherProps: P = otherPropsUntyped;

        return (
            <Component
                {...otherProps}
                getResultOfApiCall={this.getApiData}
            />
        );
    }