Simplifying Connected Props with Redux and TypeScript
Tags: react, redux, typescript, javascript
Published: 2021-06-18 13:05:57

When using Redux-connected components, there can be as many as three sources of props:

*   props passed from the parent component,
*   props returned from `mapStateToProps,`
*   props returned from `mapDispatchToProps`.

When used with TypeScript, all those props need to have types. If it's a stateful class-based component, the state needs to be typed as well. This is a lot of manual type declaration, which has to be also maintained in the future. Luckily, starting from the version 7.1.2 of `@types/react-redux` package it's possible to automatically infer types of connected props in the most cases. The way to do that is documented in the [React Redux documentation](https://react-redux.js.org/using-react-redux/usage-with-tsx#inferring-the-connected-props-automatically), and in this post we'll see the application on a concrete example. 

We'll be refactoring a sample `App` component, the implementation (but not the type) details of which are simplified for brevity. The component itself fetches a list of items on mount (via Redux action) and then renders the list, which it receives from the props. Additionally the component is using React router, where it receives the URL params as props from.
```tsx
// types.tsx
export type Item = {
  id: number;
  text: string;
};

export type AppState = {
  loading: boolean;
  data: Item[];
};

// actions.ts
export function loadData(): ThunkAction<void, AppState, undefined, PayloadAction<any>> {
  // Load data from api
}

export function deleteItem(id: string): ThunkAction<void, AppState, undefined, PayloadAction<any>> {
  // Delete an item by id
}

export function addItem(item: Item): ThunkAction<void, AppState, undefined, PayloadAction<any>> {
  // Add a new item
}

// App.tsx
import React, { useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { loadData, deleteItem, addItem } from './actions';
import { Item, AppState } from './types';

interface OwnProps extends RouteComponentProps<{ id: string }> {}

interface ConnectedProps {
  loading: boolean;
  data: Item[];
}

interface DispatchProps {
  loadData: typeof loadData;
  deleteItem: typeof deleteItem;
  addItem: typeof addItem;
}

export type Props = OwnProps & ConnectedProps & DispatchProps;

export const App = ({ loading, data, loadData, ...props }: Props) => {
  useEffect(() => {
    loadData();
  }, [loadData]);

  if (loading) {
    return <p>Loading...</p>;
  }
  return (
    <div>
      <ul>
        {data.map((result) => (
          <li key={result.id}>{result.text}</li>
        ))}
      </ul>
    </div>
  );
};

const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps> = (state: AppState, props: OwnProps) => {
  return {
    loading: state.loading,
    data: state.data,
    id: props.match.params.id,
  };
};

const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
  loadData,
  deleteItem,
  addItem,
};

export default connect(mapStateToProps, mapDispatchToProps)(App);
```
Note that we use `typeof` to infer the types of the actions and the types in `mapStateToProps` are basically a combination of `AppState` and `OwnProps` types. Looks like we're doing a lot of manual type declaration for the types we have already available elsewhere, so why not to use that type information and infer the component props automatically?

Another issue here is that the dispatched actions return a function of `ThunkAction` type, which in turn returns `void` (i.e. nothing). When connecting the component to Redux and running TypeScript in a strict mode, we get the following error: 
```tsx
Type 'Matching<ConnectedProps & { loadData: () => void; }, Props>' is not assignable to type 'DispatchProps'.           
  The types returned by 'loadData(...)' are incompatible between these types.   
     Type 'void' is not assignable to type 'ThunkAction<void, AppState, undefined, { payload: any; type: string; }>'.
```
The last part, `Type 'void' is not assignable to type 'ThunkAction<void, AppState, undefined, { payload: any; type: string; }>'.` is the most important here. Even though the type of the `loadData` is `() => ThunkAction => void`, due to the way how React-Redux resolves thunks, the actual inferred type will be `() => void.`

That's where `ConnectedProps` helper type becomes useful. It allows inferring connected types from `mapStateToProps` and `mapDispatchToProps`, plus it will correctly resolve the types for thunks. To start, let's move `mapStateToProps` and `mapDispatchToProps` to the top of the file and strip them from all the generic type declarations, as they won't be necessary anymore.
```tsx
const mapStateToProps = (state: AppState, props: OwnProps) => {
  return {
    loading: state.loading,
    data: state.data,
    id: props.match.params.id,
  };
};

const mapDispatchToProps = {
  loadData,
  deleteItem,
  addItem,
};
```
Next we need to create a `connector` function by combining the props from Redux. We do it before declaring the component since we'll use this function when creating the `Props` type.
```tsx
const connector = connect(mapStateToProps, mapDispatchToProps);
```
Now it's time to use `ConnectedProps` helper to extract the types of the connected props. Before that we'll also need to remove our `ConnectedProps` and `DispatchProps` interfaces. 
```tsx
import { ConnectedProps } from 'react-redux';

//...

type PropsFromRedux = ConnectedProps<typeof connector>;
```
And lastly, we combine these props with own props to create the `Props` type for the component. 
```tsx
interface OwnProps extends RouteComponentProps<{ id: string }> {}

type Props = PropsFromRedux & OwnProps;

export const App = ({ loading, data, loadData, ...props }: Props) => { //.. }
  
export default connector(App);
``` 
The final result will look like this.
```tsx
import React, { useEffect } from 'react';
import { ConnectedProps, connect } from 'react-redux';
import { RouteComponentProps } from 'react-router-dom';
import { loadData, deleteItem, addItem } from './actions';
import { AppState } from './types';

const mapStateToProps = (state: AppState, props: OwnProps) => {
  return {
    loading: state.loading,
    data: state.data,
    id: props.match.params.id,
  };
};

const mapDispatchToProps = {
  loadData,
  deleteItem,
  addItem,
};

const connector = connect(mapStateToProps, mapDispatchToProps);

type PropsFromRedux = ConnectedProps<typeof connector>;

interface OwnProps extends RouteComponentProps<{ id: string }> {}

export type Props = PropsFromRedux & OwnProps;

export const App = ({ loading, data, loadData, ...props }: Props) => {
  useEffect(() => {
    loadData();
  }, [loadData]);

  if (loading) {
    return <p>Loading...</p>;
  }
  return (
    <div>
      <ul>
        {data.map((result) => (
          <li key={result.id}>{result}</li>
        ))}
      </ul>
    </div>
  );
};

export default connector(App);
```
We have simplified our component by getting rid of the manual declaration of the props received from Redux. They are now inferred automatically from the types we have for them in the state and actions. This greatly improves the maintainability of the app and also fixes the issue of incorrectly inferring Redux thunk action return types.