How do you create strongly typed redux middleware in TypeScript from Redux's type definitions?

Viewed 23335

I have a TypeScript project that uses React and Redux and I'm trying to add some middleware functions. I got started by implementing one from Redux's samples like so:

// ---- middleware.ts ----
export type MiddlewareFunction = (store: any) => (next: any) => (action: any) => any;

export class MyMiddleWare {
    public static Logger: MiddlewareFunction = store => next => action => {
        // Do stuff
        return next(action);
    }
}

// ---- main.ts ---- 
import * as MyMiddleware from "./middleware";

const createStoreWithMiddleware = Redux.applyMiddleware(MyMiddleWare.Logger)(Redux.createStore);

The above works just fine but since this is TypeScript I'd like to make it strongly-typed, ideally using the types defined by Redux so I don't have to reinvent and maintain my own. So, here are the relevant excerpts from my index.d.ts file for Redux:

// ---- index.d.ts from Redux ----
export interface Action {
    type: any;
}

export interface Dispatch<S> {
    <A extends Action>(action: A): A;
}

export interface MiddlewareAPI<S> {
    dispatch: Dispatch<S>;
    getState(): S;
}

export interface Middleware {
    <S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
}

I'm trying to figure out how to bring those types into my Logger method but I'm not having much luck. It seems to me that something like this ought to work:

interface MyStore {
    thing: string;
    item: number;
}

interface MyAction extends Action {
    note: string;
}

export class MyMiddleWare {
    public static Logger: Middleware = (api: MiddlewareAPI<MyStore>) => (next: Dispatch<MyStore>) => (action: MyAction) => {
        const currentState: MyStore = api.getState();
        const newNote: string = action.note;
        // Do stuff
        return next(action);
    };
}

but instead I get this error:

Error TS2322: Type '(api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => Action' is not assignable to type 'Middleware'.
Types of parameters 'api' and 'api' are incompatible.
Type 'MiddlewareAPI' is not assignable to type 'MiddlewareAPI'.
Type 'S' is not assignable to type 'MyStore'.

I see the <S> generic declared in the type definition, but I've tried a lot of different combinations and I can't seem to figure out how to specify it as MyStore so that it is recognized as the generic type in the rest of the declarations. For example, according to the declaration api.getState() should return a MyStore object. Same thinking applies to the action type <A>, of course.

5 Answers

Here's a Middleware type which saves you from having to annotate the curried functions:

import type { Dispatch, AnyAction } from 'redux'

export interface MiddlewareAPI<S, E extends AnyAction> {
  dispatch: Dispatch<E>
  getState(): S
}

export type Middleware<S, E extends AnyAction> =
  (api: MiddlewareAPI<S, E>) =>
  (next: Dispatch<E>) =>
  (event: E) => ReturnType<Dispatch<E>>

const middleware: Middleware<MyStore, MyEvent> = (api) => (next) => (event) => {
  // ...
}
Related