Sep 21, 2019

Reduce redux-connect Typescript boilerplate

When you write React Redux connected component with Typescript, if you want a make sure your mapStatesToProps and mapDispatchToProps are typed-checked, often you need to write verbose code like below:

import * as React from 'react';
import { connect } from 'react-redux';
import { toggleTodo } from './actions';
import { selectTodoStatus, selectTodoDescription } from './selectors';
import { RootState } from './type';

interface ParentProps {
  id: string;
}

interface StoreProps {
  status: string;
  description: string;
}

interface DispatchProps {
  toggle: () => void;
}

const TodoItemView = (props: ParentProps & StoreProps & DispatchProps) => {
  return <div>...</div>;
};

const mapStatesToProps = (state: RootState, ownProps: ParentProps): StoreProps => ({
  status: selectTodoStatus(state, ownProps.id),
  description: selectTodoDescription(state, ownProps.id),
});

const mapDispatchToProps = (dispatch: any, ownProps: ParentProps): DispatchProps => ({
  toggle: () => dispatch(toggleTodo(ownProps.id)),
});

export const TodoItem = connect(mapStatesToProps, mapDispatchToProps)(TodoItemView);

And that’s a pain-in-the-ass, as your selectors and actions are already properly typed, now you need to duplicate it. In addition, everytime you want to inject a new props/new actions, you need to update both the typing and the mapProps function.

Recently, I’ve stumble upon ReturnType in Typescript, and that’s the solution to fix the boilerplate:

import * as React from 'react';
import { const connect: Connect<unknown>connect } from 'react-redux';
import type { interface Dispatch<A extends Action = UnknownAction>
A *dispatching function* (or simply *dispatch function*) is a function that accepts an action or an async action; it then may or may not dispatch one or more actions to the store. We must distinguish between dispatching functions in general and the base `dispatch` function provided by the store instance without any middleware. The base dispatch function *always* synchronously sends an action to the store's reducer, along with the previous state returned by the store, to calculate a new state. It expects actions to be plain objects ready to be consumed by the reducer. Middleware wraps the base dispatch function. It allows the dispatch function to handle async actions in addition to actions. Middleware may transform, delay, ignore, or otherwise interpret actions or async actions before passing them to the next middleware.
@templateA The type of things (actions or otherwise) which may be dispatched.
Dispatch
} from 'redux';
type type TodoStatus = "not_started" | "in_progress" | "done"TodoStatus = 'not_started' | 'in_progress' | 'done'; type
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState = {
todos: Record<string, {
    status: TodoStatus;
    description: string;
}>
todos
: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type T
Record
<string, { status: TodoStatusstatus: type TodoStatus = "not_started" | "in_progress" | "done"TodoStatus; description: stringdescription: string }>;
}; const
const toggleTodo: (id: string) => {
    type: string;
    id: string;
}
toggleTodo
= (id: stringid: string) => ({
type: stringtype: 'toggle', id: stringid, }); const const selectTodoStatus: (state: RootState, id: string) => TodoStatusselectTodoStatus = (state: RootStatestate:
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState
, id: stringid: string) => state: RootStatestate.
todos: Record<string, {
    status: TodoStatus;
    description: string;
}>
todos
[id: stringid].status: TodoStatusstatus;
const const selectTodoDescription: (state: RootState, id: string) => stringselectTodoDescription = (state: RootStatestate:
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState
, id: stringid: string) => state: RootStatestate.
todos: Record<string, {
    status: TodoStatus;
    description: string;
}>
todos
[id: stringid].description: stringdescription;
interface ParentProps { ParentProps.id: stringid: string; } const const TodoItemView: (props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>) => React.JSX.ElementTodoItemView = (
props: ParentProps & {
    status: TodoStatus;
    description: string;
} & {
    toggle: () => {
        type: string;
        id: string;
    };
}
props
: ParentProps & type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function type
ReturnType
<typeof
const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
    status: TodoStatus;
    description: string;
}
mapStatesToProps
> & type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function type
ReturnType
<typeof
const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
    toggle: () => {
        type: string;
        id: string;
    };
}
mapDispatchToProps
> // [!code highlight]
) => { return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>...</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; }; const
const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
    status: TodoStatus;
    description: string;
}
mapStatesToProps
= (state: RootStatestate:
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState
, ownProps: ParentPropsownProps: ParentProps) => ({
status: TodoStatusstatus: const selectTodoStatus: (state: RootState, id: string) => TodoStatusselectTodoStatus(state: RootStatestate, ownProps: ParentPropsownProps.ParentProps.id: stringid), description: stringdescription: const selectTodoDescription: (state: RootState, id: string) => stringselectTodoDescription(state: RootStatestate, ownProps: ParentPropsownProps.ParentProps.id: stringid), }); const
const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
    toggle: () => {
        type: string;
        id: string;
    };
}
mapDispatchToProps
= (dispatch: Dispatch<UnknownAction>dispatch: interface Dispatch<A extends Action = UnknownAction>
A *dispatching function* (or simply *dispatch function*) is a function that accepts an action or an async action; it then may or may not dispatch one or more actions to the store. We must distinguish between dispatching functions in general and the base `dispatch` function provided by the store instance without any middleware. The base dispatch function *always* synchronously sends an action to the store's reducer, along with the previous state returned by the store, to calculate a new state. It expects actions to be plain objects ready to be consumed by the reducer. Middleware wraps the base dispatch function. It allows the dispatch function to handle async actions in addition to actions. Middleware may transform, delay, ignore, or otherwise interpret actions or async actions before passing them to the next middleware.
@templateA The type of things (actions or otherwise) which may be dispatched.
Dispatch
, ownProps: ParentPropsownProps: ParentProps) => ({
toggle: () => {
    type: string;
    id: string;
}
toggle
: () =>
dispatch: Dispatch
<{
    type: string;
    id: string;
}>(action: {
    type: string;
    id: string;
}, ...extraArgs: any[]) => {
    type: string;
    id: string;
}
dispatch
(
const toggleTodo: (id: string) => {
    type: string;
    id: string;
}
toggleTodo
(ownProps: ParentPropsownProps.ParentProps.id: stringid)),
}); export const
const TodoItem: ConnectedComponent<(props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>) => React.JSX.Element, {
    id: string;
    context?: React.Context<ReactReduxContextValue<any, UnknownAction> | null> | undefined;
    store?: Store | undefined;
}>
TodoItem
=
connect<{
    status: TodoStatus;
    description: string;
}, {
    toggle: () => {
        type: string;
        id: string;
    };
}, ParentProps, RootState>(mapStateToProps: MapStateToPropsParam<{
    status: TodoStatus;
    description: string;
}, ParentProps, RootState>, mapDispatchToProps: MapDispatchToPropsNonObject<...>): InferableComponentEnhancerWithProps<...> (+15 overloads)
mapState and mapDispatch (as a function)
connect
(
const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
    status: TodoStatus;
    description: string;
}
mapStatesToProps
,
const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
    toggle: () => {
        type: string;
        id: string;
    };
}
mapDispatchToProps
)(const TodoItemView: (props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>) => React.JSX.ElementTodoItemView);

Now you doesn’t need to type StoreProps and DispatchProps manually, Typescript will infer them from your selectors and actions.

Update on 26th Jan 2020

I’ve learnt recently that react-redux actually exports a ConnectedProps type utility that will infers the correct injected props for us.

Final version:

import * as React from 'react';
import { const connect: Connect<unknown>connect, type ConnectedProps<TConnector> = TConnector extends InferableComponentEnhancerWithProps<infer TInjectedProps, any> ? unknown extends TInjectedProps ? TConnector extends InferableComponentEnhancer<...> ? TInjectedProps : never : TInjectedProps : never
Infers the type of props that a connector will inject into a component.
ConnectedProps
} from 'react-redux'; // [!code highlight]
import type { interface Dispatch<A extends Action = UnknownAction>
A *dispatching function* (or simply *dispatch function*) is a function that accepts an action or an async action; it then may or may not dispatch one or more actions to the store. We must distinguish between dispatching functions in general and the base `dispatch` function provided by the store instance without any middleware. The base dispatch function *always* synchronously sends an action to the store's reducer, along with the previous state returned by the store, to calculate a new state. It expects actions to be plain objects ready to be consumed by the reducer. Middleware wraps the base dispatch function. It allows the dispatch function to handle async actions in addition to actions. Middleware may transform, delay, ignore, or otherwise interpret actions or async actions before passing them to the next middleware.
@templateA The type of things (actions or otherwise) which may be dispatched.
Dispatch
} from 'redux';
type type TodoStatus = "not_started" | "in_progress" | "done"TodoStatus = 'not_started' | 'in_progress' | 'done'; type
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState = {
todos: Record<string, {
    status: TodoStatus;
    description: string;
}>
todos
: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type T
Record
<string, { status: TodoStatusstatus: type TodoStatus = "not_started" | "in_progress" | "done"TodoStatus; description: stringdescription: string }>;
}; const
const toggleTodo: (id: string) => {
    type: string;
    id: string;
}
toggleTodo
= (id: stringid: string) => ({
type: stringtype: 'toggle', id: stringid, }); const const selectTodoStatus: (state: RootState, id: string) => TodoStatusselectTodoStatus = (state: RootStatestate:
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState
, id: stringid: string) => state: RootStatestate.
todos: Record<string, {
    status: TodoStatus;
    description: string;
}>
todos
[id: stringid].status: TodoStatusstatus;
const const selectTodoDescription: (state: RootState, id: string) => stringselectTodoDescription = (state: RootStatestate:
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState
, id: stringid: string) => state: RootStatestate.
todos: Record<string, {
    status: TodoStatus;
    description: string;
}>
todos
[id: stringid].description: stringdescription;
interface ParentProps { ParentProps.id: stringid: string; } const const TodoItemView: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.ElementTodoItemView = (
props: ParentProps & {
    status: TodoStatus;
    description: string;
} & {
    toggle: () => {
        type: string;
        id: string;
    };
}
props
: ParentProps & type ConnectedProps<TConnector> = TConnector extends InferableComponentEnhancerWithProps<infer TInjectedProps, any> ? unknown extends TInjectedProps ? TConnector extends InferableComponentEnhancer<...> ? TInjectedProps : never : TInjectedProps : never
Infers the type of props that a connector will inject into a component.
ConnectedProps
<typeof
const connector: InferableComponentEnhancerWithProps<{
    status: TodoStatus;
    description: string;
} & {
    toggle: () => {
        type: string;
        id: string;
    };
}, ParentProps>
connector
> // [!code highlight]
) => { return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>...</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; }; const
const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
    status: TodoStatus;
    description: string;
}
mapStatesToProps
= (state: RootStatestate:
type RootState = {
    todos: Record<string, {
        status: TodoStatus;
        description: string;
    }>;
}
RootState
, ownProps: ParentPropsownProps: ParentProps) => ({
status: TodoStatusstatus: const selectTodoStatus: (state: RootState, id: string) => TodoStatusselectTodoStatus(state: RootStatestate, ownProps: ParentPropsownProps.ParentProps.id: stringid), description: stringdescription: const selectTodoDescription: (state: RootState, id: string) => stringselectTodoDescription(state: RootStatestate, ownProps: ParentPropsownProps.ParentProps.id: stringid), }); const
const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
    toggle: () => {
        type: string;
        id: string;
    };
}
mapDispatchToProps
= (dispatch: Dispatch<UnknownAction>dispatch: interface Dispatch<A extends Action = UnknownAction>
A *dispatching function* (or simply *dispatch function*) is a function that accepts an action or an async action; it then may or may not dispatch one or more actions to the store. We must distinguish between dispatching functions in general and the base `dispatch` function provided by the store instance without any middleware. The base dispatch function *always* synchronously sends an action to the store's reducer, along with the previous state returned by the store, to calculate a new state. It expects actions to be plain objects ready to be consumed by the reducer. Middleware wraps the base dispatch function. It allows the dispatch function to handle async actions in addition to actions. Middleware may transform, delay, ignore, or otherwise interpret actions or async actions before passing them to the next middleware.
@templateA The type of things (actions or otherwise) which may be dispatched.
Dispatch
, ownProps: ParentPropsownProps: ParentProps) => ({
toggle: () => {
    type: string;
    id: string;
}
toggle
: () =>
dispatch: Dispatch
<{
    type: string;
    id: string;
}>(action: {
    type: string;
    id: string;
}, ...extraArgs: any[]) => {
    type: string;
    id: string;
}
dispatch
(
const toggleTodo: (id: string) => {
    type: string;
    id: string;
}
toggleTodo
(ownProps: ParentPropsownProps.ParentProps.id: stringid)),
}); const
const connector: InferableComponentEnhancerWithProps<{
    status: TodoStatus;
    description: string;
} & {
    toggle: () => {
        type: string;
        id: string;
    };
}, ParentProps>
connector
=
connect<{
    status: TodoStatus;
    description: string;
}, {
    toggle: () => {
        type: string;
        id: string;
    };
}, ParentProps, RootState>(mapStateToProps: MapStateToPropsParam<{
    status: TodoStatus;
    description: string;
}, ParentProps, RootState>, mapDispatchToProps: MapDispatchToPropsNonObject<...>): InferableComponentEnhancerWithProps<...> (+15 overloads)
mapState and mapDispatch (as a function)
connect
(
const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
    status: TodoStatus;
    description: string;
}
mapStatesToProps
,
const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
    toggle: () => {
        type: string;
        id: string;
    };
}
mapDispatchToProps
); // [!code highlight]
export const
const TodoItem: ConnectedComponent<(props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element, {
    id: string;
    context?: React.Context<ReactReduxContextValue<any, UnknownAction> | null> | undefined;
    store?: Store | undefined;
}>
TodoItem
= const connector: <(props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element>(component: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element) => ConnectedComponent<...>connector(const TodoItemView: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.ElementTodoItemView); // [!code highlight]

Thanks for reading!

Love what you're reading? Sign up for my newsletter and stay up-to-date with my latest contents and projects.

    I won't send you spam or use it for other purposes.

    Unsubscribe at any time.