【问题标题】:How to properly type a Redux connect call?如何正确键入 Redux 连接调用?
【发布时间】:2019-06-06 22:17:06
【问题描述】:

我正在尝试将 Redux 状态存储与 TypeScript 结合使用。我正在尝试使用 Redux 的官方 Typings 并希望对 connect 方法(将 mapStatetoPropsmapDispatchToProps 与组件连接)类型安全进行整个调用。

我经常看到 mapStatetoPropsmapDispatchToProps 方法只是自定义类型并返回部分组件道具的方法,例如:

function mapStateToProps(state: IStateStore, ownProps: Partial<IComponentProps>)
  : Partial<IProjectEditorProps> {}
function mapDispatchToProps (dispatch: Dispatch, ownProps: Partial<IComponentProps>)
  : Partial<IProjectEditorProps> {}

这是有类型的并且可以工作,但不是很安全,因为它可能会实例化一个缺少道具的组件,因为 Partial 接口的使用允许不完整的定义。但是,这里需要 Partial 接口,因为您可能希望在 mapStateToProps 中定义一些道具,在 mapDispatchToProps 中定义一些道具,而不是全部在一个函数中。所以这就是我想避免这种风格的原因。

我目前尝试使用的是直接将函数嵌入到 connect 调用中,并使用 redux 提供的通用类型键入 connect 调用:

connect<IComponentProps, any, any, IStateStore>(
  (state, ownProps) => ({
    /* some props supplied by redux state */
  }),
  dispatch => ({
    /* some more props supplied by dispatch calls */
  })
)(Component);

但是,这也会引发嵌入的mapStatetoPropsmapDispatchToProps 调用未定义 all Props each 的错误,因为它们都只需要它们的一个子集,但是一起定义所有的 Props。

如何正确键入 connect 调用,以便 mapStatetoPropsmapDispatchToProps 调用是真正类型安全的,并且键入检查两种方法定义的组合值是否提供所有必需的道具,而没有定义所有必需的方法之一一次性道具?我的方法有可能吗?

【问题讨论】:

    标签: reactjs typescript redux connect


    【解决方案1】:

    真的很喜欢@NSjonas 的拆分方法,但我也会从他的第二种方法中借用一些东西来平衡实用性,不要让实现完全定义您的接口,并且在键入您的调度操作时不要过于冗长;

    import * as React from 'react';
    import { connect, Dispatch } from 'react-redux'
    import { IStateStore } from '@src/reducers';
    import { fooAction } from '@src/actions';
    
    
    interface IComponentOwnProps {
      foo: string;
    }
    
    interface IComponentStoreProps {
      bar: string;
    }
    
    interface IComponentDispatchProps {
      doFoo: (...args: Parameters<typeof fooAction>) => void;
    }
    
    type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps
    
    class IComponent extends React.Component<IComponentProps, never> {
      public render() {
        return (
          <div>
            foo: {this.props.foo}
            bar: {this.props.bar}
            <button onClick={this.props.doFoo}>Do Foo</button>
          </div>
        );
      }
    }
    
    export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
      (state, ownProps): IComponentStoreProps => {
        return {
          bar: state.bar + ownProps.foo
        };
      },
      {
          doFoo: fooAction
      }
    )(IComponent);
    
    

    【讨论】:

      【解决方案2】:

      选项1:拆分IComponentProps

      执行此操作的最简单方法可能只是为“状态派生道具”、“自己的道具”和“调度道具”定义单独的接口,然后使用intersection type 将它们连接在一起以获得IComponentProps

      import * as React from 'react';
      import { connect, Dispatch } from 'react-redux'
      import { IStateStore } from '@src/reducers';
      
      
      interface IComponentOwnProps {
        foo: string;
      }
      
      interface IComponentStoreProps {
        bar: string;
      }
      
      interface IComponentDispatchProps {
        fooAction: () => void;
      }
      
      type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps
      
      class IComponent extends React.Component<IComponentProps, never> {
        public render() {
          return (
            <div>
              foo: {this.props.foo}
              bar: {this.props.bar}
              <button onClick={this.props.fooAction}>Do Foo</button>
            </div>
          );
        }
      }
      
      export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
        (state, ownProps): IComponentStoreProps => {
          return {
            bar: state.bar + ownProps.foo
          };
        },
        (dispatch: Dispatch<IStateStore>): IComponentDispatchProps => (
          {
            fooAction: () => dispatch({type:'FOO_ACTION'})
          }
        )
      )(IComponent);
      

      我们可以像这样设置连接函数的泛型参数: &lt;TStateProps, TDispatchProps, TOwnProps, State&gt;

      选项 2:让你的函数定义你的 Props 接口

      我在野外看到的另一种选择是利用ReturnType mapped type 允许您的mapX2Props 函数实际定义它们对IComponentProps 的贡献。

      type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps;
      
      interface IComponentOwnProps {
        foo: string;
      }
      
      type IComponentStoreProps = ReturnType<typeof mapStateToProps>;
      type IComponentDispatchProps = ReturnType<typeof mapDispatchToProps>;
      
      class IComponent extends React.Component<IComponentProps, never> {
        //...
      }
      
      
      function mapStateToProps(state: IStateStore, ownProps: IComponentOwnProps) {
        return {
          bar: state.bar + ownProps.foo,
        };
      }
      
      function mapDispatchToProps(dispatch: Dispatch<IStateStore>) {
        return {
          fooAction: () => dispatch({ type: 'FOO_ACTION' })
        };
      }
      
      export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
        mapStateToProps,
        mapDispatchToProps
      )(IComponent);
      

      这里最大的优势是它减少了一点样板文件,因此当你添加一个新的映射道具时,你只有一个地方可以更新。

      我一直远离ReturnType,简化是因为让您的实现定义您的编程接口“合同”(IMO)感觉倒退。以您不希望的方式更改您的IComponentProps 几乎太容易了。

      但是,由于这里的所有内容都非常独立,因此它可能是一个可以接受的用例。

      【讨论】:

      • 所以connect 调用本身不支持检查调度和存储道具的单一状态道具?这通常可以通过自定义类型来实现,检查两个不同的集合是否分离并且还定义了所有必需的道具,或者这对于 TypeScript 是不可能的?
      • 理论上您似乎应该能够做到这一点。但是,当我尝试设置它时出现错误。我已经提交了一个问题:github.com/Microsoft/TypeScript/issues/29399
      • 谢谢,您的方法看起来应该像那样工作。将此答案标记为已接受为您答案中的代码似乎是目前可能的最佳方法。
      • @LukasBach 谢谢!实际上,我只是添加了另一个我过去见过的选项。这不是我个人走的路线,但你可能更喜欢它。
      【解决方案3】:

      一种解决方案是将您的组件属性拆分为状态、调度和自己的属性:

      import React from "react";
      import { connect } from "react-redux";
      
      import { deleteItem } from "./action";
      import { getItemById } from "./selectors";
      
      interface StateProps {
        title: string;
      }
      
      interface DispatchProps {
        onDelete: () => any;
      }
      
      interface OwnProps {
        id: string;
      }
      
      export type SampleItemProps = StateProps & DispatchProps & OwnProps;
      
      export const SampleItem: React.SFC<SampleItemProps> = props => (
        <div>
          <div>{props.title}</div>
          <button onClick={props.onDelete}>Delete</button>
        </div>
      );
      
      // You can either use an explicit mapStateToProps...
      const mapStateToProps = (state: RootState, ownProps: OwnProps) : StateProps => ({
        title: getItemById(state, ownProps.id)
      });
      
      // Ommitted mapDispatchToProps...
      
      // ... and infer the types from connects arguments ...
      export default connect(mapStateToProps, mapDispatchToProps)(SampleItem);
      
      // ... or explicitly type connect and "inline" map*To*.
      export default connect<StateProps, DispatchProps, OwnProps, RootState>(
        (state, ownProps) => ({
          title: getItemById(state, ownProps.id)
        }),
        (dispatch, ownProps) => ({
          onDelete: () => dispatch(deleteItem(ownProps.id))
        })
      )(SampleItem);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-03-27
        • 1970-01-01
        • 1970-01-01
        • 2019-03-23
        • 2021-01-01
        • 2019-11-19
        • 2019-10-12
        • 2018-06-25
        相关资源
        最近更新 更多