Redux: TypeScript: type definitions for async middleware

Created on 11 Sep 2017  路  9Comments  路  Source: reduxjs/redux

Do you want to request a feature or report a bug?
Bug?

What is the current behavior?
In the definitions file, the interface for Middleware is:

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

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

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

Since the final function in the middleware thunk is typed as Dispatch<S>, it is required to synchronously return an A action. However, custom middleware should support other behaviors. In the definitions file:

 * 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.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar.

If I attempt to add a type to the vanillaPromise from the middleware examples:

import { Middleware } from 'redux'
const middleware: Middleware = store => next => action => {
  if (typeof action.then !== 'function') {
    return next(action)
  }

  return Promise.resolve(action).then(store.dispatch)
}

TypeScript throws the following errors:

ERROR in [at-loader] ./src/index.tsx:23:7 
    TS2322: Type '<S>(store: MiddlewareAPI<S>) => (next: Dispatch<S>) => <A extends Action>(action: A) => A | Promi...' is not assignable to type 'Middleware'.
  Type '(next: Dispatch<S>) => <A extends Action>(action: A) => A | Promise<A>' is not assignable to type '(next: Dispatch<S>) => Dispatch<S>'.
    Type '<A extends Action>(action: A) => A | Promise<A>' is not assignable to type 'Dispatch<S>'.
      Type 'A | Promise<A>' is not assignable to type 'A'.
        Type 'Promise<A>' is not assignable to type 'A'.

ERROR in [at-loader] ./src/index.tsx:24:21 
    TS2339: Property 'then' does not exist on type 'A'.

What is the expected behavior?

I'd expect the Middleware interface to allow returning a promise-wrapped action, or void, or etc. I'm not sure the actual fix for this... Maybe something like:

export interface MiddlewareDispatch<S> {
    <A extends Action>(action: Promise<A> | A): Promise<A | void> | A | void;
}

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

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

This solves the type errors for the vanillaPromise example, and would also solve the problems with returning void. However I'm not certain it would cover all middleware use cases.

Which versions of Redux, and which browser and OS are affected by this issue? Did this work in previous versions of Redux?

I'm using redux 3.7.2 and typescript 2.5.2. This is the first time I've tried this.

typescript

Most helpful comment

Is there any documentation explaining the proper way to use types for custom middlewares?

All 9 comments

Actually, my suggestion isn't very good, because it complicates more simple uses. If you have:

const maybeVoid: Middleware = store => next => action => {
  if (action.type === 'IgnoreMe') {
    return
  }

  next(action)
}

TypeScript throws an error, because action may be a Promise:

ERROR in [at-loader] ./src/index.tsx:33:14 
    TS2339: Property 'type' does not exist on type 'A | Promise<A>'.
  Property 'type' does not exist on type 'Promise<A>'.

Maybe something with generics? Like:

export interface MiddlewareDispatch<S, A> {
    (action: A): A;
}

export interface MiddlewareAPI<S, A> {
  dispatch: MiddlewareDispatch<S, A>;
  getState(): S;
}

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

That supports both of the previous middleware examples:

const vanillaPromise: Middleware<State, Action | Promise<Action>> = 
  store => next => action => {
    if (typeof (action as Promise<Action>).then !== 'function') {
      return next(action)
    }

    return Promise.resolve(action).then(store.dispatch)
  }

const maybeVoid: Middleware<State, Action | void> =
  store => next => action => {
    if (!action) return
    if (action.type === 'IgnoreMe') {
      return
    }

    next(action)
  }

The current definition for Middleware is:

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

This declares that a Middleware must be a generic function, which is probably not intended. This is an error when using newer versions of TypeScript that have strict generic checks (Microsoft/TypeScript#16368).
If you meant that a middleware must be of type MiddlewareAPI<S> => Dispatch<S> => Dispatch<S> for some S but not necessarily for all, the type should be Middleware<S>.

@alexburner Does the typing on the next branch solve this for you by chance? https://github.com/reactjs/redux/blob/next/index.d.ts

Closing due to inactivity.

@timdorr this thread was closed, but is currently there any alternative or fix to this issue? I am having the exact same problem.

Check out the typings in 4.0.

Is there any documentation explaining the proper way to use types for custom middlewares?

I've also been looking for an redux-thunk middleware strict integration example for a reasonable amount of time, but no luck yet.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mickeyreiss-visor picture mickeyreiss-visor  路  3Comments

CellOcean picture CellOcean  路  3Comments

dmitry-zaets picture dmitry-zaets  路  3Comments

benoneal picture benoneal  路  3Comments

cloudfroster picture cloudfroster  路  3Comments