使用 redux 连接的 React 组件时如何保持类型安全?
How can I preserve type-safety when consuming redux-connected React component?
我一直在 React/Redux/Redux-Thunk 项目中使用 TypeScript,但我一直遇到这个问题,在 connect
ing 一个组件之后,如果没有它似乎不可能明智地使用它转换它,因为连接过程似乎无法向类型系统传达连接操作已满足部分或全部 属性 要求。例如,考虑这些 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
操作。演员阵容显然有效,但现在你有三组东西要保持同步(原始道具类型,mapStateToProps
和 mapDispatchToProps
列表,以及 "leftover Props" 类型.) 这种方法感觉冗长、容易出错,而且它还会删除其他可能有用的类型信息。
是否有更好的方法来管理 connect
ed 组件的类型?
我的理解是 connect
的第三个类型参数(在声明中命名为 TOwnProps
)应该是你的 mapStateToProps
和 [=13= 使用的任何道具的类型] 自己发挥作用。由于您的 mapStateToProps
和 mapDispatchToProps
函数不使用任何道具,您可以将此类型参数设置为 {}
,而不是 FooProps
,然后错误就会消失。 (删除显式类型参数并依赖推理将为您提供相同的最终结果。)
经过更多的挖掘,我想我已经弄明白了。在问题中显示的形式中(请注意,在其类型定义文件中调用了 connect
的 12 种可能的 call/type 模式——这只是一个),[= 有四个类型参数12=]。它们似乎代表:
- TStateProps - 一种包含您将使用
mapStateToProps
参数填充到 connect
的属性的类型。 (顺便说一句也是mapStateToProps
函数的return类型)
- TDispatchProps - 一种类型,其中包含您将使用
connect
的 mapDispatchToProps
参数填充的属性。 (顺便说一句也是mapDispatchToProps
函数的return类型)
- TOwnProps - 一种类型,其中包含要为 new 组件设置的剩余属性,该组件由
connect
调用生成(这就是我的困惑所在。)TOwnProps 是 也是 mapStateToProps
和 mapDispatchToProps
. 的 ownProps
参数的类型
- State - Redux store 模型根的类型。
没有。 3 被称为 TOwnProps
有点令人困惑,因为它暗示了 ownProps
参数与 mapStateToProps
和 mapDispatchToProps
函数的关联。我从来没有机会在实践中使用 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
被称为 TInjectedProps
和 TOwnProps
被称为 TNeedsProps
,有助于让事情更加清晰。 TStateProps & TDispatchProps
是由 connect
进入包装组件的属性 "injected",TOwnProps
是包装器仍然 "needs" 来自连接组件消费者的属性。
我的另一个认识是我在问题中的方式(即 connect<FooDataProps, FooDispatchProps, FooProps, Model>(mapStateToProps, mapDispatchToProps)
)没有意义,因为如果第三个类型参数(在语义上)应该表示 "all props, state or dispatch"类型系统可以很容易地通过 &
ing FooDataProps
和 FooDispatchProps
来实现它作为参数 #1 和参数 #2。当我使用第三种类型参数时,不会传递任何新信息。
虽然有帮助,但仅关注 TOwnProps
相对于 mapStateToProps
和 mapDispatchToProps
函数的 ownProps
参数的作用。他正确地观察到我在那些函数中没有使用 ownProps
参数,并建议我传递 {}
。该答案对于它所描述的上下文来说似乎是正确的,但它忽略了 TOwnProps
的其他作用,即决定高阶 connect
ed 组件可以接受哪些道具的决定因素。
这里的总结是 TOwnProps
在这里做双重任务,不仅作为映射函数的 ownProps
参数的类型,而且 也 作为一种类型,捕获剩余的属性以供 wrapped/connected 组件的使用者设置。
我一直在 React/Redux/Redux-Thunk 项目中使用 TypeScript,但我一直遇到这个问题,在 connect
ing 一个组件之后,如果没有它似乎不可能明智地使用它转换它,因为连接过程似乎无法向类型系统传达连接操作已满足部分或全部 属性 要求。例如,考虑这些 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
操作。演员阵容显然有效,但现在你有三组东西要保持同步(原始道具类型,mapStateToProps
和 mapDispatchToProps
列表,以及 "leftover Props" 类型.) 这种方法感觉冗长、容易出错,而且它还会删除其他可能有用的类型信息。
是否有更好的方法来管理 connect
ed 组件的类型?
我的理解是 connect
的第三个类型参数(在声明中命名为 TOwnProps
)应该是你的 mapStateToProps
和 [=13= 使用的任何道具的类型] 自己发挥作用。由于您的 mapStateToProps
和 mapDispatchToProps
函数不使用任何道具,您可以将此类型参数设置为 {}
,而不是 FooProps
,然后错误就会消失。 (删除显式类型参数并依赖推理将为您提供相同的最终结果。)
经过更多的挖掘,我想我已经弄明白了。在问题中显示的形式中(请注意,在其类型定义文件中调用了 connect
的 12 种可能的 call/type 模式——这只是一个),[= 有四个类型参数12=]。它们似乎代表:
- TStateProps - 一种包含您将使用
mapStateToProps
参数填充到connect
的属性的类型。 (顺便说一句也是mapStateToProps
函数的return类型) - TDispatchProps - 一种类型,其中包含您将使用
connect
的mapDispatchToProps
参数填充的属性。 (顺便说一句也是mapDispatchToProps
函数的return类型) - TOwnProps - 一种类型,其中包含要为 new 组件设置的剩余属性,该组件由
connect
调用生成(这就是我的困惑所在。)TOwnProps 是 也是mapStateToProps
和mapDispatchToProps
. 的 - State - Redux store 模型根的类型。
ownProps
参数的类型
没有。 3 被称为 TOwnProps
有点令人困惑,因为它暗示了 ownProps
参数与 mapStateToProps
和 mapDispatchToProps
函数的关联。我从来没有机会在实践中使用 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
被称为 TInjectedProps
和 TOwnProps
被称为 TNeedsProps
,有助于让事情更加清晰。 TStateProps & TDispatchProps
是由 connect
进入包装组件的属性 "injected",TOwnProps
是包装器仍然 "needs" 来自连接组件消费者的属性。
我的另一个认识是我在问题中的方式(即 connect<FooDataProps, FooDispatchProps, FooProps, Model>(mapStateToProps, mapDispatchToProps)
)没有意义,因为如果第三个类型参数(在语义上)应该表示 "all props, state or dispatch"类型系统可以很容易地通过 &
ing FooDataProps
和 FooDispatchProps
来实现它作为参数 #1 和参数 #2。当我使用第三种类型参数时,不会传递任何新信息。
TOwnProps
相对于 mapStateToProps
和 mapDispatchToProps
函数的 ownProps
参数的作用。他正确地观察到我在那些函数中没有使用 ownProps
参数,并建议我传递 {}
。该答案对于它所描述的上下文来说似乎是正确的,但它忽略了 TOwnProps
的其他作用,即决定高阶 connect
ed 组件可以接受哪些道具的决定因素。
这里的总结是 TOwnProps
在这里做双重任务,不仅作为映射函数的 ownProps
参数的类型,而且 也 作为一种类型,捕获剩余的属性以供 wrapped/connected 组件的使用者设置。