Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 687
Bernard Hymmen Avatar asked Jul 27 '17 00:07

Bernard Hymmen


People also ask

How do I make redux middleware?

To apply a middleware in redux, we would need to require the applyMiddleware function from the redux library. import {createStore, applyMiddleware} from "redux"; In order to check that our middleware is hooked up correctly, we can start by adding a log to display a message when an action is dispatched.

Can we use redux in TypeScript?

As of React-Redux v8, React-Redux is fully written in TypeScript, and the types are included in the published package. The types also export some helpers to make it easier to write typesafe interfaces between your Redux store and your React components.


3 Answers

MyStore is not required.

export const Logger: Middleware =   (api: MiddlewareAPI<void>) =>    (next: Dispatch<void>) =>    <A extends Action>(action: A) => {     // Do stuff    return next(action);   }; 

or

export const Logger: Middleware = api => next => action => {   // Do stuff   return next(action); }; 

Have a Nice Dev

like image 59
Jaro Avatar answered Oct 10 '22 15:10

Jaro


Here is my solution:

First is the middleware creator that accepts a todo function as input which is run as core logic for the middleware. The todo function accepts an object that encapsulates store(MiddlewareAPI<S>), next(Dispatch<S>), action(Action<S>) as well as any other your custimized parameters. Please be aware that I use as Middleware to force the middleware creator to return a Middleware. This is the magic I use to get rid of the trouble.

import { MiddlewareAPI, Dispatch, Middleware } from 'redux';
import { Action } from 'redux-actions';

export interface MiddlewareTodoParams<S> {
  store: MiddlewareAPI<S>;
  next: Dispatch<S>;
  action: Action<S>;
  [otherProperty: string]: {};
}

export interface MiddlewareTodo<S> {
  (params: MiddlewareTodoParams<S>): Action<S>;
}

// <S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
export const createMiddleware = <S>(
  todo: MiddlewareTodo<S>,
  ...args: {}[]
): Middleware => {
  return ((store: MiddlewareAPI<S>) => {
    return (next: Dispatch<S>) => {
      return action => {
        console.log(store.getState(), action.type);
        return todo({ store, next, action, ...args });
      };
    };
  // Use as Middleware to force the result to be Middleware
  }) as Middleware;
};

Second part is the definition of my todo function. In this example I write some token into cookie. It is just a POC for Middleware so I don't care about the XSS risk in my codes at all.

export type OAUTH2Token = {
  header: {
    alg: string;
    typ: string;
  };
  payload?: {
    sub: string;
    name: string;
    admin: boolean;
  };
};


export const saveToken2Cookie: MiddlewareTodo<OAUTH2Token> = params => {
  const { action, next } = params;
  if (action.type === AUTH_UPDATE_COOKIE && action.payload !== undefined) {
    cookie_set('token', JSON.stringify(action.payload));
  }
  return next(action);
};

Lastly, here is what it looks of my store configruation.

const store: Store<{}> = createStore(
  rootReducer,
  // applyMiddleware(thunk, oauth2TokenMiddleware(fetch))
  applyMiddleware(thunk, createMiddleware<OAUTH2Token>(saveToken2Cookie))
);
like image 23
杨正云 Avatar answered Oct 10 '22 15:10

杨正云


I have a solution that goes like this:

export type StateType = { thing: string, item: number };

export type ActionType =
    { type: "MY_ACTION", note: string } |
    { type: "PUSH_ACTIVITIY", activity: string };

// Force cast of generic S to my StateType
// tslint:disable-next-line:no-any
function isApi<M>(m: any): m is MiddlewareAPI<StateType> {
    return true;
}

export type MiddlewareFunction =
    (api: MiddlewareAPI<StateType>, next: (action: ActionType) => ActionType, action: ActionType) => ActionType;

export function handleAction(f: MiddlewareFunction): Middleware {
    return <S>(api: MiddlewareAPI<S>) => next => action => {
        if (isApi(api)) {
            // Force cast of generic A to my ActionType
            const _action = (<ActionType>action);
            const _next: (action: ActionType) => ActionType = a => {
                // Force cast my ActionType to generic A
                // tslint:disable-next-line:no-any
                return next(<any>a);
            };
            // Force cast my ActionType to generic A
            // tslint:disable-next-line:no-any
            return f(api, _next, _action) as any;
        } else {
            return next(action);
        }
    };
}

With the handeAction function I can now define middlewares:

// Log actions and state.thing before and after action dispatching
export function loggingMiddleware(): Middleware {
    return handleAction((api, next, action) => {
        console.log(" \nBEGIN ACTION DISPATCHING:");
        console.log(`----- Action:    ${JSON.stringify(action)}\n`);
        const oldState = api.getState();

        const retVal = next(action);

        console.log(` \n----- Old thing: ${oldState.thing}`);
        console.log(`----- New thing: ${api.getState().thing)}\n`);
        console.log("END ACTION DISPATCHING\n");

        return retVal;
    });
}

// Another middleware...
export interface DataHub = { ... }:
export function dataHandlingMiddleware(datahub: DataHub): Middleware {
    return handleAction((api, next, action) => {
        switch (action.type) {
            case "PUSH_ACTIVITY": {
                handlePushActivities(action.activity, api, /* outer parameter */ datahub);
                break;
            }
            default:
        }
        return next(action);
    });
}

Please note that the middlewares can also require additional parameters like services etc (here: DataHub), that are passed in during setup. The store setup looks like this:

import {
    Store, applyMiddleware, StoreCreator, StoreEnhancer,
    createStore, combineReducers, Middleware, MiddlewareAPI
} from "redux";

const middlewares = [
    dataHandlingMiddleware(datahub),
    loggingMiddleware()];

const rootReducer = combineReducers<StateType>({ ... });
const initialState: StateType = {};

// Trick to enable Redux DevTools with TS: see https://www.npmjs.com/package/redux-ts
const devTool = (f: StoreCreator) => {
    // tslint:disable-next-line:no-any
    return ((window as any).__REDUX_DEVTOOLS_EXTENSION__) ? (window as any).__REDUX_DEVTOOLS_EXTENSION__ : f;
};
const middleware: StoreEnhancer<StateType> = applyMiddleware(...middlewares);
const store: Store<StateType> = middleware(devTool(createStore))(rootReducer, initialState);

Hope this helps.

like image 21
Martin Backschat Avatar answered Oct 10 '22 15:10

Martin Backschat