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



我一直在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操作未填充的任何属性创建其他类型。转换显然有效,但现在有三组内容需要保持同步(原始Props类型、mapStateToPropsmapDispatchToProps列表以及"剩余Props"类型。(这种方法让人觉得冗长、容易出错,还可以擦除其他可能有用的类型信息。

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

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

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

  1. TStateProps-一个类型,包含将使用mapStateToProps参数填充到connect的属性。(顺便提一下,它也是mapStateToProps函数的返回类型(
  2. TDispatchProps-一个类型,包含将使用mapDispatchToProps参数填充到connect的属性。(顺便提一下,它也是mapDispatchToProps函数的返回类型(
  3. TOwnProps-一个包含connect调用生成的组件的剩余属性的类型(这就是我的困惑所在。(TOwnProps是,也是mapStateToPropsmapDispatchToPropsownProps参数的类型
  4. 状态-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 & TDispatchProps被称为TInjectedPropsTOwnProps被称为TNeedsProps,有助于让事情更加聚焦。TStateProps & TDispatchProps是由connect"注入"到封装组件中的属性,而TOwnProps是封装器仍然"需要"来自连接组件的消费者的属性。

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

Matt McCutchen的回答虽然很有帮助,但只关注TOwnPropsmapStateToPropsmapDispatchToProps函数的ownProps参数方面的作用。他正确地观察到我在这些函数中没有使用ownProps参数,并建议我通过{}。这个答案在它描述的上下文中似乎是正确的,但它忽略了TOwnProps的另一个作用,即决定什么道具可以被高阶connected组件接受。

这里的总结是,TOwnProps在这里执行双重任务,不仅作为映射函数的ownProps参数的类型,而且作为捕获封装/连接组件的消费者剩余设置的属性的类型。

最新更新