TypeScript 与 Redux 容器斗争
TypeScript struggles with Redux containers
我在弄清楚如何正确键入 Redux containers 时遇到了一些问题。
考虑一个可能如下所示的简单演示组件:
interface MyProps {
name: string;
selected: boolean;
onSelect: (name: string) => void;
}
class MyComponent extends React.Component<MyProps, {}> { }
从这个组件的角度来看,所有的道具都是必需的。
现在我想编写一个容器,将所有这些道具拉出状态:
function mapStateToProps(state: MyState) {
return {
name: state.my.name,
selected: state.my.selected
};
}
function mapDispatchToProps(dispatch: IDispatch) {
return {
onSelect(name: string) {
dispatch(mySelectAction(name));
}
};
}
const MyContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
这行得通,但是存在一个很大的输入问题:映射函数(mapStateToProps
和 mapDispatchToProps
)没有保护它们提供正确的数据来实现 MyProps
。这很容易出错、打字错误和重构不佳。
我可以使映射函数 return 类型 MyProps
:
function mapStateToProps(state: MyState): MyProps { }
function mapDispatchToProps(dispatch: IDispatch): MyProps { }
然而,这不起作用,除非我将所有 MyProp
道具设为可选,这样每个映射函数都可以 return 只有他们关心的部分。我不想让道具成为可选的,因为它们对于展示组件来说不是可选的。
另一种选择是将每个地图功能的道具分开,然后将它们组合成组件道具:
// MyComponent
interface MyStateProps {
name: string;
selected: boolean;
}
interface MyDispatchProps {
onSelect: (name: string) => void;
}
type MyProps = MyStateProps & MyDispatchProps;
class MyComponent extends React.Component<MyProps, {}> { }
// MyContainer
function mapStateToProps(state: MyState): MyStateProps { }
function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }
好的,这让我更接近我想要的东西,但它有点吵,它让我围绕容器的形状编写我的展示组件道具界面,我不喜欢这样。
现在第二个问题出现了。如果我想将容器放在另一个组件中怎么办:
<MyContainer />
这给出了 name
、selected
和 onSelect
都丢失的编译错误...但这是故意的,因为容器正在连接到 Redux 并提供这些。所以这让我回到了让所有组件道具都可选的状态,但我不喜欢那样,因为它们并不是真正可选的。
当 MyContainer
有一些它自己想要传入的道具时,情况会变得更糟:
<MyContainer section="somethng" />
现在我要做的是 section
MyContainer
的必需道具,而不是 MyComponent
的道具,以及 name
、selected
和 onSelect
是 MyComponent
的必需道具,但 MyContainer
的可选道具或根本不是道具。我完全不知道如何表达这个。
如有任何指导,我们将不胜感激!
你的最后一个例子是正确的。您还需要定义一个 MyOwnProps
接口,然后键入 connect
函数。
使用这些类型:https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/react-redux/react-redux.d.ts,你可以做这样的事情
interface MyProps {
section: string;
name: string;
selected: boolean;
onSelect: (name: string) => void;
}
interface MyStateProps {
name: string;
selected: boolean;
}
interface MyDispatchProps {
onSelect: (name: string) => void;
}
interface MyOwnProps {
section: string;
}
class MyComponent extends React.Component<MyProps, {}> { }
function mapStateToProps(state: MyState): MyStateProps { }
function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }
const MyContainer = connect<MyStateProps, MyDispatchProps, MyOwnProps>(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
这也让您可以在 mapStateToProps
和 mapDispatchToProps
中键入 ownProps
,例如
function mapStateToProps(state: MyState, ownProps: MyOwnProps): MyStateProps
只要定义了dispatch、state和ownProps类型,就不需要为MyProps类型定义intersection类型。这样,您可以将 MyProps 保留在自己的位置,并让容器或在没有容器的情况下使用组件的地方根据需要应用道具。我想这取决于您和您的用例 - 如果您定义 MyProps = MyStateProps & MyDispatchProps & MyOwnProps
,它会绑定到一个特定的容器,该容器不太灵活(但不那么冗长)。
作为一个解决方案,它非常冗长,但我不认为有任何方法可以绕过告诉 TypeScript 所需道具的不同部分将在不同的地方组装,并且 connect
将绑定他们在一起。
此外,为了简单起见,我通常会选择可选道具,所以我没有太多使用这种方法的经验可以分享。
我只使用 Partial<MyProps>
,其中 Partial
是定义为的内置 TypeScript 类型:
type Partial<T> = {
[P in keyof T]?: T[P];
}
它需要一个接口,并使其中的每个 属性 都是可选的。
这是我编写的 presentational/Redux-aware 组件对的示例:
/components/ConnectionPane/ConnectionPane.tsx
export interface IConnectionPaneProps {
connecting: boolean;
connected: boolean;
onConnect: (hostname: string, port: number) => void;
}
interface IState {
hostname: string;
port?: number;
}
export default class ConnectionPane extends React.Component<IConnectionPaneProps, IState> {
...
}
/containers/ConnectionPane/ConnectionPane.ts
import {connect} from 'react-redux';
import {connectionSelectors as selectors} from '../../../state/ducks';
import {connect as connectToTestEcho} from '../../../state/ducks/connection';
import {ConnectionPane, IConnectionPaneProps} from '../../components';
function mapStateToProps (state): Partial<IConnectionPaneProps> {
return {
connecting: selectors.isConnecting(state),
connected: selectors.isConnected(state)
};
}
function mapDispatchToProps (dispatch): Partial<IConnectionPaneProps> {
return {
onConnect: (hostname, port) => dispatch(connectToTestEcho(hostname, port))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ConnectionPane) as any;
演示组件道具是非可选的 - 完全符合演示要求,与相应的 "smart" 组件没有任何关系。
同时,mapStateToProps
和 mapDispatchToProps
将允许我在每个函数中分配所需的演示道具的子集,同时将演示道具界面中未定义的任何道具标记为错误。
interface MyStateProps {
name: string;
selected: boolean;
}
interface MyDispatchProps {
onSelect: (name: string) => void;
}
interface MyOwnProps {
section: string;
}
// Intersection Types
type MyProps = MyStateProps & MyDispatchProps & MyOwnProps;
class MyComponent extends React.Component<MyProps, {}> { }
function mapStateToProps(state: MyState): MyStateProps { }
function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }
const MyContainer = connect<MyStateProps, MyDispatchProps, MyOwnProps>(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
您可以使用名为 交叉点类型 的东西
https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types
我在弄清楚如何正确键入 Redux containers 时遇到了一些问题。
考虑一个可能如下所示的简单演示组件:
interface MyProps {
name: string;
selected: boolean;
onSelect: (name: string) => void;
}
class MyComponent extends React.Component<MyProps, {}> { }
从这个组件的角度来看,所有的道具都是必需的。
现在我想编写一个容器,将所有这些道具拉出状态:
function mapStateToProps(state: MyState) {
return {
name: state.my.name,
selected: state.my.selected
};
}
function mapDispatchToProps(dispatch: IDispatch) {
return {
onSelect(name: string) {
dispatch(mySelectAction(name));
}
};
}
const MyContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
这行得通,但是存在一个很大的输入问题:映射函数(mapStateToProps
和 mapDispatchToProps
)没有保护它们提供正确的数据来实现 MyProps
。这很容易出错、打字错误和重构不佳。
我可以使映射函数 return 类型 MyProps
:
function mapStateToProps(state: MyState): MyProps { }
function mapDispatchToProps(dispatch: IDispatch): MyProps { }
然而,这不起作用,除非我将所有 MyProp
道具设为可选,这样每个映射函数都可以 return 只有他们关心的部分。我不想让道具成为可选的,因为它们对于展示组件来说不是可选的。
另一种选择是将每个地图功能的道具分开,然后将它们组合成组件道具:
// MyComponent
interface MyStateProps {
name: string;
selected: boolean;
}
interface MyDispatchProps {
onSelect: (name: string) => void;
}
type MyProps = MyStateProps & MyDispatchProps;
class MyComponent extends React.Component<MyProps, {}> { }
// MyContainer
function mapStateToProps(state: MyState): MyStateProps { }
function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }
好的,这让我更接近我想要的东西,但它有点吵,它让我围绕容器的形状编写我的展示组件道具界面,我不喜欢这样。
现在第二个问题出现了。如果我想将容器放在另一个组件中怎么办:
<MyContainer />
这给出了 name
、selected
和 onSelect
都丢失的编译错误...但这是故意的,因为容器正在连接到 Redux 并提供这些。所以这让我回到了让所有组件道具都可选的状态,但我不喜欢那样,因为它们并不是真正可选的。
当 MyContainer
有一些它自己想要传入的道具时,情况会变得更糟:
<MyContainer section="somethng" />
现在我要做的是 section
MyContainer
的必需道具,而不是 MyComponent
的道具,以及 name
、selected
和 onSelect
是 MyComponent
的必需道具,但 MyContainer
的可选道具或根本不是道具。我完全不知道如何表达这个。
如有任何指导,我们将不胜感激!
你的最后一个例子是正确的。您还需要定义一个 MyOwnProps
接口,然后键入 connect
函数。
使用这些类型:https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/react-redux/react-redux.d.ts,你可以做这样的事情
interface MyProps {
section: string;
name: string;
selected: boolean;
onSelect: (name: string) => void;
}
interface MyStateProps {
name: string;
selected: boolean;
}
interface MyDispatchProps {
onSelect: (name: string) => void;
}
interface MyOwnProps {
section: string;
}
class MyComponent extends React.Component<MyProps, {}> { }
function mapStateToProps(state: MyState): MyStateProps { }
function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }
const MyContainer = connect<MyStateProps, MyDispatchProps, MyOwnProps>(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
这也让您可以在 mapStateToProps
和 mapDispatchToProps
中键入 ownProps
,例如
function mapStateToProps(state: MyState, ownProps: MyOwnProps): MyStateProps
只要定义了dispatch、state和ownProps类型,就不需要为MyProps类型定义intersection类型。这样,您可以将 MyProps 保留在自己的位置,并让容器或在没有容器的情况下使用组件的地方根据需要应用道具。我想这取决于您和您的用例 - 如果您定义 MyProps = MyStateProps & MyDispatchProps & MyOwnProps
,它会绑定到一个特定的容器,该容器不太灵活(但不那么冗长)。
作为一个解决方案,它非常冗长,但我不认为有任何方法可以绕过告诉 TypeScript 所需道具的不同部分将在不同的地方组装,并且 connect
将绑定他们在一起。
此外,为了简单起见,我通常会选择可选道具,所以我没有太多使用这种方法的经验可以分享。
我只使用 Partial<MyProps>
,其中 Partial
是定义为的内置 TypeScript 类型:
type Partial<T> = {
[P in keyof T]?: T[P];
}
它需要一个接口,并使其中的每个 属性 都是可选的。
这是我编写的 presentational/Redux-aware 组件对的示例:
/components/ConnectionPane/ConnectionPane.tsx
export interface IConnectionPaneProps {
connecting: boolean;
connected: boolean;
onConnect: (hostname: string, port: number) => void;
}
interface IState {
hostname: string;
port?: number;
}
export default class ConnectionPane extends React.Component<IConnectionPaneProps, IState> {
...
}
/containers/ConnectionPane/ConnectionPane.ts
import {connect} from 'react-redux';
import {connectionSelectors as selectors} from '../../../state/ducks';
import {connect as connectToTestEcho} from '../../../state/ducks/connection';
import {ConnectionPane, IConnectionPaneProps} from '../../components';
function mapStateToProps (state): Partial<IConnectionPaneProps> {
return {
connecting: selectors.isConnecting(state),
connected: selectors.isConnected(state)
};
}
function mapDispatchToProps (dispatch): Partial<IConnectionPaneProps> {
return {
onConnect: (hostname, port) => dispatch(connectToTestEcho(hostname, port))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ConnectionPane) as any;
演示组件道具是非可选的 - 完全符合演示要求,与相应的 "smart" 组件没有任何关系。
同时,mapStateToProps
和 mapDispatchToProps
将允许我在每个函数中分配所需的演示道具的子集,同时将演示道具界面中未定义的任何道具标记为错误。
interface MyStateProps {
name: string;
selected: boolean;
}
interface MyDispatchProps {
onSelect: (name: string) => void;
}
interface MyOwnProps {
section: string;
}
// Intersection Types
type MyProps = MyStateProps & MyDispatchProps & MyOwnProps;
class MyComponent extends React.Component<MyProps, {}> { }
function mapStateToProps(state: MyState): MyStateProps { }
function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }
const MyContainer = connect<MyStateProps, MyDispatchProps, MyOwnProps>(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
您可以使用名为 交叉点类型 的东西 https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types