使用 TypeScript 编写 React 高阶组件

Writing a React higher-order component with TypeScript

我正在使用 TypeScript 编写 React higher-order component (HOC)。 HOC 应该比包装组件多接受一个 prop,所以我这样写:

type HocProps {
    // Contains the prop my HOC needs
    thingy: number
}
type Component<P> = React.ComponentClass<P> | React.StatelessComponent<P>
interface ComponentDecorator<TChildProps> {
    (component: Component<TChildProps>): Component<HocProps & TChildProps>;
}
const hoc = function<TChildProps>(): (component: Component<TChildProps>) => Component<HocProps & TChildProps) {
    return (Child: Component<TChildProps>) => {
        class MyHOC extends React.Component<HocProps & TChildProps, void> {
            // Implementation skipped for brevity
        }
        return MyHOC;
    }
}
export default hoc;

换句话说,hoc 是一个产生实际 HOC 的函数。这个 HOC 是(我相信)一个接受 Component 的函数。因为我事先不知道包装组件是什么,所以我使用通用类型 TChildProps 来定义包装组件的 props 形状。函数也returns一个Component。返回的组件接受包装组件的道具(同样,使用通用 TChildProps 键入) 它自己需要的一些道具(键入 HocProps)。使用返回的组件时,应提供所有道具(HocProps 和包装的道具 Component)。

现在,当我尝试使用我的 HOC 时,我会执行以下操作:

// outside parent component
const WrappedChildComponent = hoc()(ChildComponent);

// inside parent component
render() {
    return <WrappedChild
                thingy={ 42 } 
                // Prop `foo` required by ChildComponent
                foo={ 'bar' } />
}

但是我收到 TypeScript 错误:

TS2339: Property 'foo' does not exist on type 'IntrinsicAttributes & HocProps & {} & { children? ReactNode; }'

在我看来,TypeScript 并没有用 ChildComponent 所需道具的形状替换 TChildProps。我怎样才能让 TypeScript 做到这一点?

我找到了一种使其工作的方法:通过使用提供的类型参数调用 hoc,如下所示:

import ChildComponent, { Props as ChildComponentProps } from './child';
const WrappedChildComponent = hoc<ChildComponentProps>()(ChildComponent);

但我不是很喜欢。它要求我导出 child 的道具(我宁愿不这样做)而且我觉得我正在告诉 TypeScript 它应该能够推断出的东西。

如果您要求的是是否可以定义一个可以添加新道具的 HOC,比方说 "thingy",无需修改组件的道具定义以包含 "thingy" ] 我觉得这不可能。

那是因为在代码中的某个时刻你会得到:

render() {
    return (
        <WrappedComponent thingy={this.props.thingy} {...this.props}/>
    );
}

如果 WrappedComponent 在其 props 定义中不包含 thingy,那将始终抛出错误。 child 必须知道它收到了什么。副手,我想不出将道具传递给无论如何都不知道的组件的原因。如果没有错误,您将无法在 child 组件中引用该道具。

我认为诀窍是将 HOC 定义为围绕 child 的道具的泛型,然后只包含您的道具 thingy 或 child 中的任何东西显式接口。

interface HocProps {
    // Contains the prop my HOC needs
    thingy: number;
}

const hoc = function<P extends HocProps>(
    WrappedComponent: new () => React.Component<P, any>
) {
    return class MyHOC extends React.Component<P, any> {
        render() {
            return (
                <WrappedComponent {...this.props}/>
            );
        }
    }
}
export default hoc;

// Example child class

// Need to make sure the Child class includes 'thingy' in its props definition or
// this will throw an error below where we assign `const Child = hoc(ChildClass)`
interface ChildClassProps {
    thingy: number;
}

class ChildClass extends React.Component<ChildClassProps, void> {
    render() {
        return (
            <h1>{this.props.thingy}</h1>
        );
    }
}

const Child = hoc(ChildClass);

当然,这个 HOC 示例并没有真正做 任何事情。真正的 HOC 应该做一些逻辑来为 child 属性提供一个值。例如,您可能有一个组件显示一些重复更新的通用数据。您可以采用不同的方式来更新它并创建 HOC 来分离该逻辑。

您有一个组件:

interface ChildComponentProps {
    lastUpdated: number;
    data: any;
}

class ChildComponent extends React.Component<ChildComponentProps, void> {
    render() {
        return (
            <div>
                <h1>{this.props.lastUpdated}</h1>
                <p>{JSON.stringify(this.props.data)}</p>
            </div>
        );
    }
}

然后使用 setInterval 以固定间隔更新 child 组件的示例 HOC 可能是:

interface AutoUpdateProps {
    lastUpdated: number;
}

export function AutoUpdate<P extends AutoUpdateProps>(
    WrappedComponent: new () => React.Component<P, any>,
    updateInterval: number
) {
    return class extends React.Component<P, any> {
        autoUpdateIntervalId: number;
        lastUpdated: number;

        componentWillMount() {
            this.lastUpdated = 0;
            this.autoUpdateIntervalId = setInterval(() => {
                this.lastUpdated = performance.now();
                this.forceUpdate();
            }, updateInterval);
        }

        componentWillUnMount() {
            clearInterval(this.autoUpdateIntervalId);
        }

        render() {
            return (
                <WrappedComponent lastUpdated={this.lastUpdated} {...this.props}/>
            );
        }
    }
}

然后我们可以创建一个组件,像这样每秒更新一次 child:

const Child = AutoUpdate(ChildComponent, 1000);

你的问题是 hoc 是通用的,但没有参数——没有数据可以推断出任何东西。这迫使您在调用它时明确指定 hoc 的类型。即使您执行 hoc()(Foo),Typescript 也必须首先评估 hoc()——并且此时要继续的信息为零。因此需要 hoc<FooProps>()(Foo).

如果 hoc 的主体不需要类型并且可以在这一点上灵活,您可以使 hoc not 成为通用的,而是 return 通用函数(高阶组件)。

也就是说,而不是

const hoc = function<TChildProps>(): (component: Component<TChildProps>) => Component<HocProps & TChildProps) {
    return (Child: Component<TChildProps>) => {

你可以改为

const hoc = function(): <TChildProps>(component: Component<TChildProps>) => Component<HocProps & TChildProps) {
    return <TChildProps>(Child: Component<TChildProps>) => {

(注意 <TChildProps> 的位置)

然后当你调用hoc()时,你得到一个泛型函数,当你调用hoc()(Foo)时,它会被推断为等价于hoc()<FooProps>(Foo)

如果 hoc 本身需要对类型参数做一些事情,这对你没有帮助——如果需要,你是在说“给我一个函数,它只接受具有这种类型的组件的参数作为a 属性”,你需要给它类型 then。这是否有帮助取决于实际的 hoc 功能在做什么,这在您的问题中没有得到证明。

我假设您的实际用例 hoc 中做某事,而不是立即 return 调用高阶组件函数。当然,如果没有,你应该去掉间接层,只剩下高阶组件。即使您 hoc 内做事,我也强烈建议您很有可能 而不是 这样做,将该功能移动到实际的高阶组件中,并消除间接。你的方法只有在你调用 hoc 一次或几次时才有意义,但随后使用不同参数多次使用生成的 hoc() 函数——即使那样,这也是一个可能为时过早的优化。

晚会有点晚了。 我喜欢使用 Omit TypeScript 实用程序类型来解决这个问题。 Link 到文档:https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk

import React, {ComponentType} from 'react';

export interface AdditionalProps {
    additionalProp: string;
}

export function hoc<P extends AdditionalProps>(WrappedComponent: ComponentType<P>) : ComponentType<Omit<P, 'additionalProp'>> {
    const additionalProp = //...
    return props => (
        <WrappedComponent
            additionalProp={additionalProp}
            {...props as any}
        />
    );
}

我发现以下关于 Pluralsight 的文章对于理解 Typescript 中的 HOC 及其基本概念非常有帮助。我遇到的所有其他文章过去都是混杂在一起的东西,而这只是工作,一步一步,清晰简洁:

Link to the article.