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;
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?
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.
You have a Store interface, but no State interface or Type
Hi,
Thanks for the articele, but it would be real nice to checkout complete example on GitHub…
export type Store = {
myData: {
clickCount: number,
label: string
}
}
could be:
export type Store =ReadOnly
Long story short: you saved my live! thanks
What is your type for State:
state: State
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.
In fact upon closer inspection, this entire solution does not work with the latest typescript.
yes
Maybe you wanna try passing a fourth type in connect which will be your state interface:
export default connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
interface State {
…your state types
}
export default connect — StateFromProps, DispatchFromProps, { label: string }, State — (
mapStateToProps,
mapDispatchToProps
)(MyComponent);
This got things working, thanks!!!
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
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);
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!
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);
Of course angle bracket has been cut out by text area:
class MyComponent extends React.Component { … }
Just imagine there: open_angle_bracket ComponentProps, ComponentState angle_bracket_close
Nice post, thank you ! I had to change `void` to `{}` in `connect` to get it working.
*My text has been cut, it’s in `connect StateFromProps, DispatchFromProps, void`
This was super useful thank you so much!