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 { connect } from 'react-redux';
import type { Dispatch } from 'redux';
type TodoStatus = 'not_started' | 'in_progress' | 'done';
type const connect: Connect<unknown>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.type TodoStatus = "not_started" | "in_progress" | "done"type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState = {
todos: Record<string, { status: TodoStatus; description: string }>;
};
const toggleTodo = (id: string) => ({
type: 'toggle',
id,
});
const selectTodoStatus = (state: RootState, id: string) => state.todos[id].status;
const selectTodoDescription = (state: RootState, id: string) => state.todos[id].description;
interface ParentProps {
id: string;
}
const TodoItemView = (
props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>
) => {
return <div>...</div>;
};
const mapStatesToProps = (state: RootState, ownProps: ParentProps) => ({
status: selectTodoStatus(state, ownProps.id),
description: selectTodoDescription(state, ownProps.id),
});
const mapDispatchToProps = (dispatch: Dispatch, ownProps: ParentProps) => ({
toggle: () => dispatch(toggleTodo(ownProps.id)),
});
export const TodoItem = connect(mapStatesToProps, mapDispatchToProps)(TodoItemView);
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 { connect, ConnectedProps } from 'react-redux';
import type { Dispatch } from 'redux';
type TodoStatus = 'not_started' | 'in_progress' | 'done';
type
todos: Record<string, {
status: TodoStatus;
description: string;
}>
type Record<K extends keyof any, T> = { [P in K]: T; }Construct a type with a set of properties K of type Tstatus: TodoStatustype TodoStatus = "not_started" | "in_progress" | "done"description: stringconst toggleTodo: (id: string) => {
type: string;
id: string;
}
id: stringtype: stringid: stringconst selectTodoStatus: (state: RootState, id: string) => TodoStatusstate: RootStatetype RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: stringstate: RootStatetodos: Record<string, {
status: TodoStatus;
description: string;
}>
id: stringstatus: TodoStatusconst selectTodoDescription: (state: RootState, id: string) => stringstate: RootStatetype RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: stringstate: RootStatetodos: Record<string, {
status: TodoStatus;
description: string;
}>
id: stringdescription: stringParentProps.id: stringconst TodoItemView: (props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>) => React.JSX.Elementprops: ParentProps & {
status: TodoStatus;
description: string;
} & {
toggle: () => {
type: string;
id: string;
};
}
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyObtain the return type of a function typeconst mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
status: TodoStatus;
description: string;
}
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyObtain the return type of a function typeconst mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
toggle: () => {
type: string;
id: string;
};
}
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
status: TodoStatus;
description: string;
}
state: RootStatetype RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
ownProps: ParentPropsstatus: TodoStatusconst selectTodoStatus: (state: RootState, id: string) => TodoStatusstate: RootStateownProps: ParentPropsParentProps.id: stringdescription: stringconst selectTodoDescription: (state: RootState, id: string) => stringstate: RootStateownProps: ParentPropsParentProps.id: stringconst mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
toggle: () => {
type: string;
id: string;
};
}
dispatch: Dispatch<UnknownAction>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.ownProps: ParentPropstoggle: () => {
type: string;
id: string;
}
dispatch: Dispatch
<{
type: string;
id: string;
}>(action: {
type: string;
id: string;
}, ...extraArgs: any[]) => {
type: string;
id: string;
}
const toggleTodo: (id: string) => {
type: string;
id: string;
}
ownProps: ParentPropsParentProps.id: stringconst 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;
}>
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)const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
status: TodoStatus;
description: string;
}
const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
toggle: () => {
type: string;
id: string;
};
}
const TodoItemView: (props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>) => React.JSX.Elementconst connect: Connect<unknown>type ConnectedProps<TConnector> = TConnector extends InferableComponentEnhancerWithProps<infer TInjectedProps, any> ? unknown extends TInjectedProps ? TConnector extends InferableComponentEnhancer<...> ? TInjectedProps : never : TInjectedProps : neverInfers the type of props that a connector will inject into a component.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.type TodoStatus = "not_started" | "in_progress" | "done"type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState = {
todos: Record<string, { status: TodoStatus; description: string }>;
};
const toggleTodo = (id: string) => ({
type: 'toggle',
id,
});
const selectTodoStatus = (state: RootState, id: string) => state.todos[id].status;
const selectTodoDescription = (state: RootState, id: string) => state.todos[id].description;
interface ParentProps {
id: string;
}
const TodoItemView = (
props: ParentProps & ConnectedProps<typeof connector>
) => {
return <div>...</div>;
};
const mapStatesToProps = (state: RootState, ownProps: ParentProps) => ({
status: selectTodoStatus(state, ownProps.id),
description: selectTodoDescription(state, ownProps.id),
});
const mapDispatchToProps = (dispatch: Dispatch, ownProps: ParentProps) => ({
toggle: () => dispatch(toggleTodo(ownProps.id)),
});
const connector = connect(mapStatesToProps, mapDispatchToProps);
export const TodoItem = connector(TodoItemView);
todos: Record<string, {
status: TodoStatus;
description: string;
}>
type Record<K extends keyof any, T> = { [P in K]: T; }Construct a type with a set of properties K of type Tstatus: TodoStatustype TodoStatus = "not_started" | "in_progress" | "done"description: stringconst toggleTodo: (id: string) => {
type: string;
id: string;
}
id: stringtype: stringid: stringconst selectTodoStatus: (state: RootState, id: string) => TodoStatusstate: RootStatetype RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: stringstate: RootStatetodos: Record<string, {
status: TodoStatus;
description: string;
}>
id: stringstatus: TodoStatusconst selectTodoDescription: (state: RootState, id: string) => stringstate: RootStatetype RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: stringstate: RootStatetodos: Record<string, {
status: TodoStatus;
description: string;
}>
id: stringdescription: stringParentProps.id: stringconst TodoItemView: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Elementprops: ParentProps & {
status: TodoStatus;
description: string;
} & {
toggle: () => {
type: string;
id: string;
};
}
type ConnectedProps<TConnector> = TConnector extends InferableComponentEnhancerWithProps<infer TInjectedProps, any> ? unknown extends TInjectedProps ? TConnector extends InferableComponentEnhancer<...> ? TInjectedProps : never : TInjectedProps : neverInfers the type of props that a connector will inject into a component.const connector: InferableComponentEnhancerWithProps<{
status: TodoStatus;
description: string;
} & {
toggle: () => {
type: string;
id: string;
};
}, ParentProps>
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
status: TodoStatus;
description: string;
}
state: RootStatetype RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
ownProps: ParentPropsstatus: TodoStatusconst selectTodoStatus: (state: RootState, id: string) => TodoStatusstate: RootStateownProps: ParentPropsParentProps.id: stringdescription: stringconst selectTodoDescription: (state: RootState, id: string) => stringstate: RootStateownProps: ParentPropsParentProps.id: stringconst mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
toggle: () => {
type: string;
id: string;
};
}
dispatch: Dispatch<UnknownAction>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.ownProps: ParentPropstoggle: () => {
type: string;
id: string;
}
dispatch: Dispatch
<{
type: string;
id: string;
}>(action: {
type: string;
id: string;
}, ...extraArgs: any[]) => {
type: string;
id: string;
}
const toggleTodo: (id: string) => {
type: string;
id: string;
}
ownProps: ParentPropsParentProps.id: stringconst connector: InferableComponentEnhancerWithProps<{
status: TodoStatus;
description: string;
} & {
toggle: () => {
type: string;
id: string;
};
}, ParentProps>
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)const mapStatesToProps: (state: RootState, ownProps: ParentProps) => {
status: TodoStatus;
description: string;
}
const mapDispatchToProps: (dispatch: Dispatch, ownProps: ParentProps) => {
toggle: () => {
type: string;
id: string;
};
}
const TodoItem: ConnectedComponent<(props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element, {
id: string;
context?: React.Context<ReactReduxContextValue<any, UnknownAction> | null> | undefined;
store?: Store | undefined;
}>
const connector: <(props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element>(component: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element) => ConnectedComponent<...>const TodoItemView: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element 