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: TodoStatus
type TodoStatus = "not_started" | "in_progress" | "done"
description: string
const toggleTodo: (id: string) => {
type: string;
id: string;
}
id: string
type: string
id: string
const selectTodoStatus: (state: RootState, id: string) => TodoStatus
state: RootState
type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: string
state: RootState
todos: Record<string, {
status: TodoStatus;
description: string;
}>
id: string
status: TodoStatus
const selectTodoDescription: (state: RootState, id: string) => string
state: RootState
type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: string
state: RootState
todos: Record<string, {
status: TodoStatus;
description: string;
}>
id: string
description: string
ParentProps.id: string
const TodoItemView: (props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>) => React.JSX.Element
props: ParentProps & {
status: TodoStatus;
description: string;
} & {
toggle: () => {
type: string;
id: string;
};
}
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain 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 : any
Obtain 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: RootState
type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
ownProps: ParentProps
status: TodoStatus
const selectTodoStatus: (state: RootState, id: string) => TodoStatus
state: RootState
ownProps: ParentProps
ParentProps.id: string
description: string
const selectTodoDescription: (state: RootState, id: string) => string
state: RootState
ownProps: ParentProps
ParentProps.id: string
const 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: ParentProps
toggle: () => {
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: ParentProps
ParentProps.id: string
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;
}>
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.Element
const connect: Connect<unknown>
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.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: TodoStatus
type TodoStatus = "not_started" | "in_progress" | "done"
description: string
const toggleTodo: (id: string) => {
type: string;
id: string;
}
id: string
type: string
id: string
const selectTodoStatus: (state: RootState, id: string) => TodoStatus
state: RootState
type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: string
state: RootState
todos: Record<string, {
status: TodoStatus;
description: string;
}>
id: string
status: TodoStatus
const selectTodoDescription: (state: RootState, id: string) => string
state: RootState
type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
id: string
state: RootState
todos: Record<string, {
status: TodoStatus;
description: string;
}>
id: string
description: string
ParentProps.id: string
const TodoItemView: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element
props: 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 : never
Infers 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: RootState
type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
ownProps: ParentProps
status: TodoStatus
const selectTodoStatus: (state: RootState, id: string) => TodoStatus
state: RootState
ownProps: ParentProps
ParentProps.id: string
description: string
const selectTodoDescription: (state: RootState, id: string) => string
state: RootState
ownProps: ParentProps
ParentProps.id: string
const 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: ParentProps
toggle: () => {
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: ParentProps
ParentProps.id: string
const 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