Skip to content

Instantly share code, notes, and snippets.

Created Aug 21, 2021
What would you like to do?
Type safe Redux helpers
/* eslint-disable @typescript-eslint/generic-type-naming */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ActionCreatorsMapObject, Middleware } from "redux";
import { isPlainObject } from "lodash";
// An interface to define action classes
export interface IAction {
readonly type: string;
type ActionCreatorResponse<
TReturn extends (...args: any[]) => any
> = ReturnType<ReturnType<TReturn>>;
type IsValidArg<T> = T extends object
? keyof T extends never
? false
: true
: true;
/* A helper type to infer the correct return type of an async action.
* An async action creator returns an async function which returns a promise or a plain Redux action.
* Without this helper async actions types will have the actions themselves as return types.
* Instead, we need to return a value returned from the async action.
* This helper removes the intemediate functions definitions and leaves the plain return types only.
type ReplaceReturnType<T, TNewReturn> = T extends (
a: infer A,
b: infer B,
c: infer C,
d: infer D,
e: infer E,
f: infer F,
g: infer G,
h: infer H,
i: infer I,
j: infer J,
) => infer R
? (IsValidArg<J> extends true
? (
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
g: G,
h: H,
i: I,
j: J,
) => TNewReturn
: IsValidArg<I> extends true
? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => TNewReturn
: IsValidArg<H> extends true
? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => TNewReturn
: IsValidArg<G> extends true
? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => TNewReturn
: IsValidArg<F> extends true
? (a: A, b: B, c: C, d: D, e: E, f: F) => TNewReturn
: IsValidArg<E> extends true
? (a: A, b: B, c: C, d: D, e: E) => TNewReturn
: IsValidArg<D> extends true
? (a: A, b: B, c: C, d: D) => TNewReturn
: IsValidArg<C> extends true
? (a: A, b: B, c: C) => TNewReturn
: IsValidArg<B> extends true
? (a: A, b: B) => TNewReturn
: IsValidArg<A> extends true
? (a: A) => TNewReturn
: () => TNewReturn)
: never;
/* A helper type to create an action creators object map type
* which we can export from the core to use it in the outer layers.
export type MakeBoundActionCreatorsMap<T extends ActionCreatorsMapObject> = {
[K in keyof T]: ReplaceReturnType<T[K], ActionCreatorResponse<T[K]>>
/* A helper type to make a union type with all the actions.
* We use the union type in a reducer to infer actions types
export type ActionsUnion<A extends ActionCreatorsMapObject> = Extract<
ReturnType<A[keyof A]>,
// Binds all of the methods of an object to the object
export function createActionsObject<T extends ActionCreatorsMapObject>(
actions: T,
): T {
Object.keys(actions).forEach(action => {
actions[action] = actions[action].bind(actions);
return actions;
// Middleware to convert action classes' instances to plain objects
export const plainObjectMiddleware: Middleware<
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
> = () => next => action => {
if (typeof action !== "function" && !isPlainObject(action)) {
return next(Object.assign({}, action));
return next(action);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment