React 带属性 + Redux connect() 的高阶组件正确 Typing 方式

考虑这样的一个场景。我们有一个高阶组件 WrappedComponent,它接受一个属性类型为 BaseProps 的组件 Component,然后做以下事情:

  • WrappedComponent 的属性类型为 WrappedComponentProps
  • 向其中注入新的属性,属性类型为 InjectedProps
  • 将该组件与返回值类型为 IStatePropsmapStateToProps、类型为 IDispatchPropsmapDispatchToProps 连接 (connect)
  • 在生命周期中添加一些可复用的逻辑

当我们用 JS 的时候,上面的需求很简单:

import React from 'react';
import { connect } from 'react-redux';

import { increaseCount } from './actions';

const withPropsAndConnect = (Component) => {
  const injectedProps = {
    /* Injected props */
  };

  class WrappedComponent extends React.Component {
    componentDidUpdate() {
      /* ....... */
    }

    render() {
      const childProps = {
        ...injectedProps,
        ...this.props
      };
      return <Component {...childProps} />
    }
  }

  const mapStateToProps = (state) => {
    return {
      count: state.rootReducer.count,
      // redux mapStateToProps
    };
  };

  const mapDispatchToProps = {
    increase: () => increaseCount()
  };

  return connect(mapStateToProps, mapDispatchToProps)(WrappedComponent);
}

然而,当我们用 typescript 的时候,这件事就变得十分地麻烦,反正我看着一整页的 typescript 报错,脑子里只有 “ybb”:

image.png

经过了一整个晚上的冲浪,终于找到了正确的写法。这里需要借助 utility-types 包的工具泛型 Diff<U, T>

import React from 'react';
import { connect } from 'react-redux';
import { Diff } from 'utility-types';

import { increaseCount } from './actions';
import { AppState } from './store/types';         // redux store 状态树类型

export const withPropsAndConnect = <BaseProps extends object>(Component: React.ComponentType<BaseProps>) => {
  const mapStateToProps = (state: AppState) => ({
    count: state.rootReducer.count,
  });
  const mapDispatchToProps = {
    increase: () => increaseCount()
  };

  const injectedProps: InjectProps = {
    // ...
  };

  type IStateProps = ReturnType<typeof mapStateToProps>;
  type IDispatchProps = typeof mapDispatchToProps;

  type ComponentProps = IStateProps & IDispatchProps & WrappedComponentProps;
  type ComponentState = WrapperBaseStates;

  class WrappedComponent extends React.Component<ComponentProps, ComponentState> {
    constructor(props: ComponentProps) {
      super(props);
  
      this.state = { /* ... */ };
  
    componentDidUpdate(prevProps: ComponentProps) {
      /* ... */
    }
  
    render() {
      const childProps = {
        ...injectedProps,
        ...this.props
      };
      return (
        <Component
          {...(injectedProps as BaseProps)}
        />
      );
    }
  }

  type HOCProps = Diff<BaseProps, object>;

  return connect<IStateProps, IDispatchProps, HOCProps, AppState>
      (mapStateToProps, mapDispatchToProps)(WrappedComponent);
}