Typesafe Container Components with React-Redux’s Connect and TypeScript

TypeScript is fantastic—you get feedback as you write your code to (mostly) ensure you’re passing valid arguments to functions and props to components. However, when using the connect function, you may find that you are permitted to pass invalid props or omit required props to components. In this post, I’ll discuss how the issue arises, and how to fix it by typing your calls to connect!

Setting Up the Component

If you are using TypeScript and React, you more than likely provide types for your component props. It’s easy to do, and it ensures that your components are being provided the right props. Here’s a simple example:



interface MyProps {
  label: string;
  clickCount: number;
  handleClick: () => void;
}

export default class MyComponent extends React.Component {
  render() {
    const { label, clickCount, handleClick } = this.props;
    return (
      <div>
        <div onClick={handleClick}>
          {label}
        </div>
        <div>
          Clicks: {clickCount}
        </div>
      </div>
    );
  }
}

If you are getting your data from a Redux store, you may wrap this presentation component in a container component generated by connect. I’m going to do some hand-waving and assume we’ve already defined types for the Redux store’s state(if not, check the addition at the bottom of the post) and set up our actions and reducers. Given that, we could create the following container as follows:



const mapStateToProps = (state: State) => ({
  clickCount: state.myData.clickCount
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
  handleClick: () => dispatch(incrementCounterAction())
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

Then elsewhere in the codebase, we could bring this in using:



<MyContainer />

It Looks Good, But…

At this point, your text editor should show no errors, and your code should happily transpile. There’s just one problem: The label prop is never passed to the presentation component!

Assuming we expect the label to be provided by the container, I should have added another line in mapStateToProps so it looked like this:



const mapStateToProps = (state: State) => ({
  label: state.myData.label,
  clickCount: state.myData.clickCount
});

Unfortunately, TypeScript does not catch this error. We can remedy this, though, by providing connect with types.

Typing Connect

As a starting point, I’ll add types to mapStateToProps and mapDispatchToProps.



interface StateFromProps {
  label: string;
  clickCount: number;
}

interface DispatchFromProps {
  handleClick: () => void;
}

The next step is to add types to connect. The connect accepts two arguments, mapStateToProps and mapDispatchToProps, returns a function that accepts a Component type, and returns another component. We need to provide types for the two arguments, and the props of the component that will be returned. For the time being, I’ll just add types for mapStateToProps and mapDispatchToProps.



export default connect<StateFromProps, DispatchFromProps, void>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

With this in place, TypeScript will recognize errors if you do not return the correct data types from mapStateToProps and mapDispatchToProps.

Passing Props to the Container

In the example above, the third type I gave to connect was void, indicating no props needed to be passed to the component it generates. Let’s say, though, that you would like to pass label through the container, rather than get it from the store. To accomplish that, just remove the label from mapStateToProps (reverting back to its original version). And instead of void, pass the type { label: string }.

We now have something like this:



const mapStateToProps = (state: State) => ({
  clickCount: state.myData.clickCount
});

const mapDispatchToProps = (dispatch: Dispatch): DispatchFromProps => ({
  handleClick: () => dispatch(incrementCounterAction())
});

export default connect<StateFromProps, DispatchFromProps, { label: string }>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

And we can use it like this:



<MyContainer label={'It works!'} />

There you have it—typesafe container components generated through connect.

Typing the Store and Dispatch

To revisit the above hand-waving on typing the store, here’s how you could type the store:



export type Store = {
  myData: {
    clickCount: number,
    label: string
  }
}

And dispatch with actions:



type ActionA = {
  type: 'INCREMENT_COUNTER'
}
type Action = ActionTypeA | OtherAction;
export type Dispatch = (action: Action) => void;

Conversation
  • Fasoro-Joseph Alexander says:

    Hi, I am really new to typescript in Redux and am unsure of how to define types for the Redux store’s state. This is exactly what led me to the article and I was immediately pulling my hair out when I saw that ‘Hand wave’ over that aspect. Could yo show an example of how that can be achieved?

    • Tyler Hoffman Tyler Hoffman says:

      You can just define it like any other type and then use it shown in the post. I added an update to the bottom of the post to show an example of typing the store and dispatch. Hope that helps.

  • Pasha says:

    Hi,
    Thanks for the articele, but it would be real nice to checkout complete example on GitHub…

  • THomas says:

    export type Store = {
    myData: {
    clickCount: number,
    label: string
    }
    }
    could be:
    export type Store =ReadOnly

  • Dani says:

    Long story short: you saved my live! thanks

  • Nikos says:

    What is your type for State:

    state: State

  • Steve says:

    Several issues here. In your first snippet, you say the Compiler doesn’t complain. Well yes it does in typescript 2.6: Your deconstructed props are not recognized as you have not declared them as implicit Types on the class , nor passed them to the constructor.

    Also where on earth do you declare you State type in your mapStateToProps. I came here hopefully looking for a solution to a wide spread problem with React Redux, connect and Typescript, however, it seems this still shows the same issues. No idea how yours compiles and works (if it does), would be nice to see a github repo where we could see it in action.

    • Steve says:

      In fact upon closer inspection, this entire solution does not work with the latest typescript.

  • Walter says:

    yes

  • Robert says:

    Maybe you wanna try passing a fourth type in connect which will be your state interface:

    export default connect(
    mapStateToProps,
    mapDispatchToProps
    )(MyComponent);

    • Robert says:

      interface State {
      …your state types
      }

      export default connect — StateFromProps, DispatchFromProps, { label: string }, State — (
      mapStateToProps,
      mapDispatchToProps
      )(MyComponent);

  • Christian Chown says:

    Thank you for this post Tyler – yours is the first article that let me to:

    export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

    I’d been struggling to get correctly typed connect()s until now – you just saved me (more) hours

    • Christian Chown says:

      apologies – the formatter removed the angled bracket part (the most important bit!) from my post… substitute hyphens in this snippet:

      export default connect – StateFromProps, DispatchFromProps, OwnProps, ReduxStoreState – (mapStateToProps, mapDispatchToProps)(MyComponent);

  • Aldrost says:

    Great article! I was bumping my head about why I was having to specify all properties of the connected component when using it even though I provided them in mapState/DispatchToProps. This article cleared it for me that I had to give `connect` the types of the objects returned from those two functions.

    Thanks again!

  • Fzr says:

    Hi Tyler,
    What do yo think about TypeScript intersection (“&”) here?
    It can satisfy props from JSX and those custom Redux.
    Example:
    interface BasicComponentProps extends React.Props { … }
    type MyReduxProps { … };
    type ComponentProps = BasicComponentProps & MyReduxProps;

    class MyComponent extends React.Component { … }

    const mapStateToProps = (store: MyAppState): MyReduxProps => { … };
    export default connect(mapStateToProps)(MyComponent);

  • Fzr says:

    Of course angle bracket has been cut out by text area:
    class MyComponent extends React.Component { … }

  • Fzr says:

    Just imagine there: open_angle_bracket ComponentProps, ComponentState angle_bracket_close

  • soywod says:

    Nice post, thank you ! I had to change `void` to `{}` in `connect` to get it working.

    • soywod says:

      *My text has been cut, it’s in `connect StateFromProps, DispatchFromProps, void`

  • Audrey says:

    This was super useful thank you so much!

  • Comments are closed.