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

How can I preserve type-safety when consuming redux-connected React component?

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

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) 使所有道具可选。 我从第三方看到的许多组件都是可选的,但根据我的经验,一切都是可选的会导致很多样板 nil-checks 无处不在,使代码更难阅读。

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

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

我的理解是 connect 的第三个类型参数(在声明中命名为 TOwnProps)应该是你的 mapStateToProps 和 [=13= 使用的任何道具的类型] 自己发挥作用。由于您的 mapStateToPropsmapDispatchToProps 函数不使用任何道具,您可以将此类型参数设置为 {},而不是 FooProps,然后错误就会消失。 (删除显式类型参数并依赖推理将为您提供相同的最终结果。)

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

  1. TStateProps - 一种包含您将使用 mapStateToProps 参数填充到 connect 的属性的类型。 (顺便说一句也是mapStateToProps函数的return类型)
  2. TDispatchProps - 一种类型,其中包含您将使用 connectmapDispatchToProps 参数填充的属性。 (顺便说一句也是mapDispatchToProps函数的return类型)
  3. TOwnProps - 一种类型,其中包含要为 new 组件设置的剩余属性,该组件由 connect 调用生成(这就是我的困惑所在。)TOwnProps 是 也是 mapStateToPropsmapDispatchToProps.
  4. ownProps 参数的类型
  5. State - Redux store 模型根的类型。

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

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 被称为 TInjectedPropsTOwnProps 被称为 TNeedsProps,有助于让事情更加清晰。 TStateProps & TDispatchProps 是由 connect 进入包装组件的属性 "injected",TOwnProps 是包装器仍然 "needs" 来自连接组件消费者的属性。

我的另一个认识是我在问题中的方式(即 connect<FooDataProps, FooDispatchProps, FooProps, Model>(mapStateToProps, mapDispatchToProps))没有意义,因为如果第三个类型参数(在语义上)应该表示 "all props, state or dispatch"类型系统可以很容易地通过 &ing FooDataPropsFooDispatchProps 来实现它作为参数 #1 和参数 #2。当我使用第三种类型参数时,不会传递任何新信息。

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

这里的总结是 TOwnProps 在这里做双重任务,不仅作为映射函数的 ownProps 参数的类型,而且 作为一种类型,捕获剩余的属性以供 wrapped/connected 组件的使用者设置。