Jan 26, 2020 First publish on 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.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 TRecord<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 : anyObtain the return type of a function typeReturnType<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 : anyObtain the return type of a function typeReturnType<typeof const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
toggle: () => {
type: string;
id: string;
};
}
mapDispatchToProps>
) => {
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.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<{
toggle: () => {
type: string;
id: string;
};
}, ParentProps>): InferableComponentEnhancerWithProps<{
status: TodoStatus;
description: string;
} & {
toggle: () => {
type: string;
id: string;
};
}, ParentProps> (+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<infer TInjectedProps> ? TInjectedProps : never : TInjectedProps : neverInfers the type of props that a connector will inject into a component.ConnectedProps } 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.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 TRecord<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<infer TInjectedProps> ? TInjectedProps : never : TInjectedProps : neverInfers 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>
) => {
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.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<{
toggle: () => {
type: string;
id: string;
};
}, ParentProps>): InferableComponentEnhancerWithProps<{
status: TodoStatus;
description: string;
} & {
toggle: () => {
type: string;
id: string;
};
}, ParentProps> (+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);
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<(props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element, {
id: string;
context?: React.Context<ReactReduxContextValue<any, UnknownAction> | null> | undefined;
store?: Store | undefined;
}>
connector(const TodoItemView: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.ElementTodoItemView);
