【问题标题】:How can I preserve type-safety when consuming redux-connected React component?使用 redux 连接的 React 组件时如何保持类型安全?
【发布时间】:2018-10-25 19:12:42
【问题描述】:

我一直在 React/Redux/Redux-Thunk 项目中使用 TypeScript,我一直遇到这个问题,在 connecting 一个组件之后,如果不强制转换它似乎不可能明智地使用它,因为连接过程似乎无法向类型系统传达连接操作已满足部分或全部属性要求。例如,考虑这些组件/类型/等:

import * as React from 'react';
import {connect} from "react-redux";
import {Action, bindActionCreators, Dispatch} from "redux";
import {ThunkDispatch} from "redux-thunk";

// Our store model
interface Model {
    name: string,
}

// Types for our component's props
interface FooDataProps {
    name: string // Single, required, string property
}

interface FooDispatchProps {
    onClick: React.MouseEventHandler<HTMLButtonElement>, // Single, required, event handler.
}

interface FooProps extends FooDataProps, FooDispatchProps { // Union the two types
}

// Make our first component...
function TrivialComponent(props: FooProps) {
    return (<button onClick={props.onClick}>{props.name}</button>);
}

// Now make a Redux "container" that wires it to the store...
const mapStateToProps = (state: Model): FooDataProps => { return { name: state.name }; };
const mapDispatchToProps = (dispatch: Dispatch): FooDispatchProps => {
    return bindActionCreators({onClick: doStuff}, dispatch);
};

// Wire it up with all the glory of the heavily-genericized `connect`
const ConnectedTrivialComponent = connect<FooDataProps, FooDispatchProps, FooProps, Model>(mapStateToProps, mapDispatchToProps)(TrivialComponent);

// Then let's try to consume it
function ConsumingComponent1() {
    // At this point, I shouldn't need to provide any props to the ConnectedTrivialComponent -- they're 
    // all being provided by the `connect` hookup, but if I try to use the tag like I'm doing here, I 
    // get this error: 
    //
    // Error:(53, 10) TS2322: Type '{}' is not assignable to type 'Readonly<Pick<FooProps, never> & FooProps>'.
    // Property 'name' is missing in type '{}'.
    //
    return (<ConnectedTrivialComponent/>)
}

// If I do something like this:
const ConnectedTrivialComponent2 = ConnectedTrivialComponent as any as React.ComponentClass<{}, {}>;

// Then let's try to consume it
function ConsumingComponent2() {
    // I can do this no problem.
    return (<ConnectedTrivialComponent2/>)
}

// Handler...
const doStuff = (e: React.MouseEvent<HTMLButtonElement>) => (dispatch: ThunkDispatch<Model, void, Action>, getStore: () => Model) => {
    // Do stuff
};

好的,所以,在考虑这个问题时,我已经通过了一些想法:

想法 #1)让所有的 props 都是可选的。 我从第三方看到的很多组件都是可选的,但是根据我的经验,让所有的都是可选的会导致很多样板的 nil-check all在这个地方,并使代码更难阅读。

想法#2)转换为React.ComponentClass&lt;P,S&gt;,并为connect 操作填充的任何属性创建其他类型。演员阵容显然有效,但现在你有三组东西要相互保持同步(原始道具类型,mapStateToPropsmapDispatchToProps 列表,以及“剩余道具”类型。)这种方法感觉冗长、容易出错,而且还会删除其他可能有用的类型信息。

有没有更好的方法来管理connected 组件的类型?

【问题讨论】:

    标签: reactjs typescript redux redux-thunk


    【解决方案1】:

    我的理解是connect 的第三个类型参数(在声明中命名为TOwnProps)应该是您的mapStateToPropsmapDispatchToProps 函数本身使用的任何道具的类型。由于您的mapStateToPropsmapDispatchToProps 函数不使用任何道具,您可以将此类型参数设置为{},而不是FooProps,然后错误就会消失。 (删除显式类型参数并依赖推理将得到相同的最终结果。)

    【讨论】:

      【解决方案2】:

      经过更多的挖掘,我想我已经弄清楚了。在问题中显示的形式中(请注意,在其类型定义文件中调用了 connect 的 12 种可能的调用/类型模式——这只是一种),connect 有四个类型参数。它们似乎代表:

      1. TStateProps - 一个类型,包含您将使用mapStateToProps 参数填充到connect 的属性。 (这也是mapStateToProps函数的返回类型)
      2. TDispatchProps - 一种类型,包含您将使用mapDispatchToProps 参数填充到connect 的属性。 (这也是mapDispatchToProps函数的返回类型)
      3. TOwnProps - 一种类型,包含由connect 调用生成的 new 组件的剩余属性(这是我的困惑所在。)TOwnProps 是 ownProps参数的类型改为mapStateToPropsmapDispatchToProps
      4. State - Redux 存储模型根的类型。

      没有。 3 被称为TOwnProps 有点令人困惑,因为它暗示了ownProps 参数与mapStateToPropsmapDispatchToProps 函数的关联。在实践中我从来没有机会使用ownProps,所以我没有立即明白它实际上还有更多。再挖一挖,发现connect返回:

      InferableComponentEnhancerWithProps<TStateProps & TDispatchProps, TOwnProps>
      

      它的定义是:

      export interface InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> {
          <C extends ComponentType<Matching<TInjectedProps, GetProps<C>>>>(
              component: C
          ): ConnectedComponentClass<C, Omit<GetProps<C>, keyof Shared<TInjectedProps, GetProps<C>>> & TNeedsProps>
      }
      

      看到TStateProps &amp; TDispatchProps 被称为TInjectedPropsTOwnProps 被称为TNeedsProps,有助于让事情更加集中。 TStateProps &amp; TDispatchPropsconnect“注入”到被包装组件中的属性,TOwnProps 是包装器仍然“需要”来自连接组件的消费者的属性。

      我的另一个认识是我在问题中的方式(即connect&lt;FooDataProps, FooDispatchProps, FooProps, Model&gt;(mapStateToProps, mapDispatchToProps))没有意义,因为如果第三个类型参数(语义上)应该代表“所有道具、状态或调度”类型系统可以很容易地通过&amp;ing FooDataPropsFooDispatchProps 来实现它作为参数#1 和参数#2。在我使用第三个类型参数时,不会传达任何新信息。

      Matt McCutchen's answer 虽然很有帮助,但仅关注TOwnProps 相对于ownProps 参数对mapStateToPropsmapDispatchToProps 函数的作用。他正确地观察到我在这些函数中没有使用ownProps 参数,并建议我传递{}。该答案对于它所描述的上下文似乎是正确的,但它忽略了TOwnProps 的其他角色,即高阶connected 组件可以接受哪些道具的决定因素。

      这里的总结是 TOwnProps 在这里做双重职责,不仅作为映射函数的 ownProps 参数的类型,而且也是作为捕获剩余属性的类型由包装/连接组件的消费者设置。

      【讨论】:

        猜你喜欢
        • 2019-09-05
        • 2017-09-16
        • 1970-01-01
        • 2017-09-07
        • 2019-05-09
        • 2017-04-13
        • 2019-01-09
        • 2023-03-30
        • 1970-01-01
        相关资源
        最近更新 更多