【问题标题】:Writing a React higher-order component with TypeScript使用 TypeScript 编写 React 高阶组件
【发布时间】:2017-09-26 14:33:36
【问题描述】:

我正在用 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 来定义被包装组件的道具的形状。该函数还返回一个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 并没有将 TChildProps 替换为 ChildComponent 所需的道具形状。我怎样才能让 TypeScript 做到这一点?

【问题讨论】:

    标签: reactjs typescript higher-order-components


    【解决方案1】:

    聚会有点晚了。 我喜欢使用 Omit TypeScript 实用程序类型来解决这个问题。 文档链接: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}
            />
        );
    }
    

    【讨论】:

      【解决方案2】:

      如果您要问的是是否可以定义一个 HOC 来添加一个新的道具,比如说“thingy”,而不修改该组件的道具定义以包含“thingy”,我认为这是不可能的。

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

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

      如果 WrappedComponent 在其 props 定义中不包含 thingy,那将总是抛出错误。孩子必须知道它收到了什么。顺便说一句,我想不出将道具传递给一个无论如何都不知道的组件的原因。您将无法在子组件中引用该道具而不会出错。

      我认为诀窍是将 HOC 定义为围绕孩子的道具的泛型,然后在孩子的界面中明确包含您的道具 thingy 或其他任何内容。

      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 应该做一些逻辑来为子道具提供价值。例如,也许您有一个组件,该组件显示一些重复更新的通用数据。您可以采用不同的方式更新它并创建 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 以固定间隔更新子组件的示例 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}/>
                  );
              }
          }
      }
      

      然后我们可以创建一个每秒更新一次子元素的组件,如下所示:

      const Child = AutoUpdate(ChildComponent, 1000);
      

      【讨论】:

        【解决方案3】:

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

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

        但我真的不喜欢它。它需要我导出孩子的道具(我不想这样做),我觉得我在告诉 TypeScript 它应该能够推断出的东西。

        【讨论】:

          【解决方案4】:

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

          Link to the article.

          【讨论】:

          • 这篇文章很棒!
          【解决方案5】:

          您的问题是 hoc 是通用的,但没有参数——没有数据可以推断出任何东西。这会迫使您在调用 hoc 时显式指定其类型。即使你使用hoc()(Foo),Typescript 也必须首先评估hoc()——此时需要进行的信息为零。因此需要hoc&lt;FooProps&gt;()(Foo)

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

          也就是说,而不是

          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>) => {
          

          (注意&lt;TChildProps&gt;的位置)

          那么当你调用hoc()时,你得到一个泛型函数,当你调用hoc()(Foo)时,它会被推断为等价于hoc()&lt;FooProps&gt;(Foo)

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

          我假设您的实际用例正在hoc 中做某事,而不是立即返回高阶组件函数。当然,如果没有,您应该只删除间接层并只保留高阶组件。即使您正在hoc 内部做事,我强烈建议您最好这样做,将功能移到内部实际的高阶组件,并消除了间接性。只有当您调用 hoc 一次或几次时,您的方法才有意义,但随后使用生成的 hoc() 函数多次使用不同的参数 - 即使这样,这也可能是一种优化为时过早。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2019-07-25
            • 2020-08-31
            • 2021-08-15
            • 2018-05-23
            • 2020-02-06
            • 2019-10-25
            • 2018-09-17
            • 2019-04-30
            相关资源
            最近更新 更多