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: TodoStatus
status: type TodoStatus = "not_started" | "in_progress" | "done"
TodoStatus; description: string
description: string }>;
};
const const toggleTodo: (id: string) => {
type: string;
id: string;
}
toggleTodo = (id: string
id: string) => ({
type: string
type: 'toggle',
id: string
id,
});
const const selectTodoStatus: (state: RootState, id: string) => TodoStatus
selectTodoStatus = (state: RootState
state: type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState, id: string
id: string) => state: RootState
state.todos: Record<string, {
status: TodoStatus;
description: string;
}>
todos[id: string
id].status: TodoStatus
status;
const const selectTodoDescription: (state: RootState, id: string) => string
selectTodoDescription = (state: RootState
state: type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState, id: string
id: string) => state: RootState
state.todos: Record<string, {
status: TodoStatus;
description: string;
}>
todos[id: string
id].description: string
description;
interface ParentProps {
ParentProps.id: string
id: string;
}
const const TodoItemView: (props: ParentProps & ReturnType<typeof mapStatesToProps> & ReturnType<typeof mapDispatchToProps>) => React.JSX.Element
TodoItemView = (
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 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 : any
Obtain 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: RootState
state: type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState, ownProps: ParentProps
ownProps: ParentProps) => ({
status: TodoStatus
status: const selectTodoStatus: (state: RootState, id: string) => TodoStatus
selectTodoStatus(state: RootState
state, ownProps: ParentProps
ownProps.ParentProps.id: string
id),
description: string
description: const selectTodoDescription: (state: RootState, id: string) => string
selectTodoDescription(state: RootState
state, ownProps: ParentProps
ownProps.ParentProps.id: string
id),
});
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: ParentProps
ownProps: 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: ParentProps
ownProps.ParentProps.id: string
id)),
});
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.Element
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 { 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';
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: TodoStatus
status: type TodoStatus = "not_started" | "in_progress" | "done"
TodoStatus; description: string
description: string }>;
};
const const toggleTodo: (id: string) => {
type: string;
id: string;
}
toggleTodo = (id: string
id: string) => ({
type: string
type: 'toggle',
id: string
id,
});
const const selectTodoStatus: (state: RootState, id: string) => TodoStatus
selectTodoStatus = (state: RootState
state: type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState, id: string
id: string) => state: RootState
state.todos: Record<string, {
status: TodoStatus;
description: string;
}>
todos[id: string
id].status: TodoStatus
status;
const const selectTodoDescription: (state: RootState, id: string) => string
selectTodoDescription = (state: RootState
state: type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState, id: string
id: string) => state: RootState
state.todos: Record<string, {
status: TodoStatus;
description: string;
}>
todos[id: string
id].description: string
description;
interface ParentProps {
ParentProps.id: string
id: string;
}
const const TodoItemView: (props: ParentProps & ConnectedProps<typeof connector>) => React.JSX.Element
TodoItemView = (
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>
) => {
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: RootState
state: type RootState = {
todos: Record<string, {
status: TodoStatus;
description: string;
}>;
}
RootState, ownProps: ParentProps
ownProps: ParentProps) => ({
status: TodoStatus
status: const selectTodoStatus: (state: RootState, id: string) => TodoStatus
selectTodoStatus(state: RootState
state, ownProps: ParentProps
ownProps.ParentProps.id: string
id),
description: string
description: const selectTodoDescription: (state: RootState, id: string) => string
selectTodoDescription(state: RootState
state, ownProps: ParentProps
ownProps.ParentProps.id: string
id),
});
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: ParentProps
ownProps: 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: ParentProps
ownProps.ParentProps.id: string
id)),
});
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);
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.Element
TodoItemView);