问题 TypeScript与Redux容器竞争


我在弄清楚如何正确打字时遇到了一些麻烦 Redux容器

考虑一个简单的表示组件,可能如下所示:

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。这容易出错,拼写错误和重构不佳。

我可以使映射函数返回类型 MyProps

function mapStateToProps(state: MyState): MyProps { }

function mapDispatchToProps(dispatch: IDispatch): MyProps { }

然而,除非我全力以赴,否则这不起作用 MyProp props可选,这样每个映射函数只能返回它们关心的部分。我不想让道具成为可选的,因为它们不是演示组件的可选项。

另一种选择是为每个地图功能拆分道具,并将它们组合成组件道具:

// 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 />

这给出了编译错误 nameselected,和 onSelect 都缺失了...但这是故意的,因为容器连接到Redux并提供这些。所以这促使我回到使所有组件道具可选,但我不喜欢它,因为它们不是真正可选的。

事情变得更糟 MyContainer 有一些自己想要传递的道具:

<MyContainer section="somethng" />

现在我要做的就是拥有 section 一个必需的支柱 MyContainer 但不是支柱 MyComponent,和 nameselected,和 onSelect 是必需的道具 MyComponent 但可选或不是道具 MyContainer。我完全不知道如何表达这一点。

任何指导都将不胜感激!


12660
2017-11-18 07:58


起源



答案:


你的最后一个例子是正确的。你还需要定义的是一个 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);

这也让你输入 ownProps 在 mapStateToProps 和 mapDispatchToProps,例如

function mapStateToProps(state: MyState, ownProps: MyOwnProps): MyStateProps

只要定义dispatch,state和ownProps类型,就不需要为MyProps类型定义交集类型。这样,您可以将MyProps保留在自己的位置,让容器或组件在没有容器的地方使用,根据需要应用道具。我想这取决于你和你的用例 - 如果你定义的话 MyProps = MyStateProps & MyDispatchProps & MyOwnProps,它与一个特定的容器绑在一起,这个容器灵活性较差(但不那么冗长)。

作为一个解决方案,它非常冗长,但我认为没有办法告诉TypeScript不同的所需道具将在不同的地方组装,并且 connect将他们绑在一起。

此外,为了它的价值,我通常选择道具,为了简单起见,所以我没有太多的经验来分享使用这种方法。


12
2017-11-18 15:44



谢谢,这有帮助。我想没有TypeScript,一切都是可选的。我只是喜欢组件道具可以告诉你什么是预期的东西与哪些东西是可选的。 - Aaron
问题:因为这个解决方案基本上意味着你定义每个道具两次(一次进入 MyProps,并在适当的一次 MyStateProps 要么 MyDispatchProps),如果你重命名或更改道具,会发生什么 MyProps, 如 name 至 id 要么 selected: boolean 至 selected: SelectionEnum?类型检查器是否意识到这一点 MyOwnProps, MyDispatchProps, MyStateProps 不满足 MyProps 了吗? - Aaron
有些。自写答案以来,我一直在玩它。 State和Dispatch道具必须匹配,但OwnProps不必匹配。我想也许这是因为只能在mapStateToProps和mapDispatchToProps中使用ownProps而不能在MyComponent中使用它们? - Radio-
错误就像 (123,3): error TS2345: Argument of type 'typeof FeatureTab' is not assignable to parameter of type 'ComponentClass<IStateToProps & IDispatchToProps> | StatelessComponent<IStateToProps & IDispatchTo...'... - Radio-
哈,我不认为我知道这个错误在说什么......它是否会逐渐变成有用的东西 'name' exists in {...} but not in {...}? - Aaron


答案:


你的最后一个例子是正确的。你还需要定义的是一个 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);

这也让你输入 ownProps 在 mapStateToProps 和 mapDispatchToProps,例如

function mapStateToProps(state: MyState, ownProps: MyOwnProps): MyStateProps

只要定义dispatch,state和ownProps类型,就不需要为MyProps类型定义交集类型。这样,您可以将MyProps保留在自己的位置,让容器或组件在没有容器的地方使用,根据需要应用道具。我想这取决于你和你的用例 - 如果你定义的话 MyProps = MyStateProps & MyDispatchProps & MyOwnProps,它与一个特定的容器绑在一起,这个容器灵活性较差(但不那么冗长)。

作为一个解决方案,它非常冗长,但我认为没有办法告诉TypeScript不同的所需道具将在不同的地方组装,并且 connect将他们绑在一起。

此外,为了它的价值,我通常选择道具,为了简单起见,所以我没有太多的经验来分享使用这种方法。


12
2017-11-18 15:44



谢谢,这有帮助。我想没有TypeScript,一切都是可选的。我只是喜欢组件道具可以告诉你什么是预期的东西与哪些东西是可选的。 - Aaron
问题:因为这个解决方案基本上意味着你定义每个道具两次(一次进入 MyProps,并在适当的一次 MyStateProps 要么 MyDispatchProps),如果你重命名或更改道具,会发生什么 MyProps, 如 name 至 id 要么 selected: boolean 至 selected: SelectionEnum?类型检查器是否意识到这一点 MyOwnProps, MyDispatchProps, MyStateProps 不满足 MyProps 了吗? - Aaron
有些。自写答案以来,我一直在玩它。 State和Dispatch道具必须匹配,但OwnProps不必匹配。我想也许这是因为只能在mapStateToProps和mapDispatchToProps中使用ownProps而不能在MyComponent中使用它们? - Radio-
错误就像 (123,3): error TS2345: Argument of type 'typeof FeatureTab' is not assignable to parameter of type 'ComponentClass<IStateToProps & IDispatchToProps> | StatelessComponent<IStateToProps & IDispatchTo...'... - Radio-
哈,我不认为我知道这个错误在说什么......它是否会逐渐变成有用的东西 'name' exists in {...} but not in {...}? - Aaron


我只是用 Partial<MyProps>,哪里 Partial 是一种内置的TypeScript类型,定义如下:

type Partial<T> = {
    [P in keyof T]?: T[P];
}

它需要一个接口,并使其中的每个属性都是可选的。

这是我编写的表示/ Redux感知组件对的示例:

/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;

演示组件道具是非可选的 - 完全按照演示文稿的要求而不对相应的“智能”组件产生任何影响。

与此同时, mapStateToProps 和 mapDispatchToProps 将允许我在每个函数中分配所需的表示道具的子集,同时将在展示道具界面中未定义的任何道具标记为错误。


2
2018-06-27 17:14



是的,自从TS发布以来 Partial 支持我一直在一些项目中使用它。它易于使用。它不像正式那样严格 StateProps, DispatchProps 和 OwnProps 方法,因为它不能确保映射函数完成,只是它不会返回错误实现的prop。我玩弄了玩具 Pick 撬开一个表现组件道具界面,但这仍然是相当乏味的。 - Aaron
让一切都是可选的不是解决方案,因为问题是明确反对它。 - bastianwegge
@wegginho OP写道“然而,除非我全力以赴,否则这不起作用 MyProp props可选,这样每个映射函数只能返回它们关心的部分。我不想制作道具 [MyProp] 可选“。我的建议是 不 制作 MyProp 可选的或实际上对表示组件或其界面进行任何改变。这是包装 MyProp 在一个类型中,它将所有属性公开为可选,仅用于Redux类型。这是IMO的理想解决方案,因为它允许每个Redux函数处理一部分道具,同时保护未知道具。 - Tagc
如果你没有传递必需的道具,@ Tagc会抱怨吗? - bastianwegge
Wegginho是对的,这不是 理想 我正在寻找的解决方案,因为它确实使映射函数的所有道具都可选,即地图函数不能确保返回它们应该的所有内容。然而,它明显比明确使组件道具所有可选更好,它是很好的替代品 简单 使用所以我赞成它。 :) - Aaron


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


1
2017-07-04 06:58



谢谢你的这个例子。它基本上是我原始尝试(使用交集类型)和@ radio接受的答案(使用 connect() 类型args)。 - Aaron