在使用Redux连接的React组件时,如何保持类型安全?

时间:2018-10-25 19:12:42

标签: reactjs typescript redux redux-thunk

我一直在React / Redux / Redux-Thunk项目中使用TypeScript,但我一直遇到这个障碍,connect在使用组件后,如果没有它,似乎无法明智地使用它进行转换,因为连接过程似乎无法向类型系统传达连接操作已满足部分或全部属性要求。例如,考虑以下组件/类型/等:

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)使所有道具都是可选的。我从第三方那里看到的许多组件都具有可选的功能,但以我的经验,使所有可选的功能会导致很多样板检查无效到处都是,使代码更难阅读。

想法#2)投射到React.ComponentClass<P,S> ,并为connect操作填充的所有 not 属性创建其他类型。强制转换很明显,但是现在您需要三套东西才能保持同步(原始的Props类型,mapStateToPropsmapDispatchToProps列表以及“剩余的Props”类型)。 )这种方法比较冗长,容易出错,并且还会删除其他可能有用的类型信息。

是否有更好的方法来管理类型的connect组件?

2 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:0)

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

  1. TStateProps -一种类型,其中包含您将使用connect参数mapStateToProps填充的属性。 (这也是connect函数的返回类型)
  2. TDispatchProps -一种类型,其中包含您将使用mapStateToProps参数填充到mapDispatchToProps的属性。 (这也是connect函数的返回类型)
  3. TOwnProps -一种类型,其中包含需要为mapDispatchToProps调用生成的 new 组件设置的属性(这是我的困惑所在。) TOwnProps也是connectownProps的{​​{1}}参数的类型。
  4. 状态-Redux存储模型根目录的类型。

不。 3被称为mapStateToProps有点令人困惑,因为它暗示了与mapDispatchToPropsTOwnProps函数的ownProps参数的关联。我从来没有机会在实践中使用mapStateToProps,所以我没有立即理解实际上还有更多的东西。进行更多的挖掘,我发现mapDispatchToProps返回:

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 & TDispatchProps被称为TInjectedProps,有助于使事情更加集中。 TOwnPropsTNeedsProps“注入”到包装组件中的属性,而TStateProps & TDispatchProps是包装器仍“需要”连接的组件使用者的属性。

我的另一种认识是,我在问题中拥有它的方式(即connect)是没有意义的,因为如果第三个类型参数(在语义上)应该表示“所有道具,状态或派遣, “通过TOwnProps connect<FooDataProps, FooDispatchProps, FooProps, Model>(mapStateToProps, mapDispatchToProps)&的类型系统可以很容易地实现它成为参数#1和参数#2。当我使用第三个类型参数时,不会传递任何新信息。

Matt McCutchen's answer尽管很有帮助,但仅关注FooDataPropsFooDispatchPropsTOwnProps函数的ownProps参数的作用。他正确地观察到我在那些函数中没有使用mapStateToProps参数,并建议我传递mapDispatchToProps。该答案似乎对它所描述的上下文是正确的,但它忽略了ownProps的其他角色,它不能确定更高阶{}组件可以接受哪些道具。

这里的总结是TOwnProps在这里有双重职责,不仅是作为地图函数的connect参数的类型,而且 作为一种类型捕获要由包装/连接的组件的使用者设置的剩余属性。