Redux: Typescript & Middleware extensions

Created on 10 May 2016  Â·  7Comments  Â·  Source: reduxjs/redux

Hi,

I'm wondering how the typescript type definitions for extension via middleware are supposed to work (when extending the types of Actions that are possible).

For instance, let's take the module redux-thunk (one of the simplest redux middlewares).

It allows you to dispatch a function with type: (Dispatch, ()=>S) => void, (that is, a function whose first parameter is the dispatch function, and whose second parameter is the getState function).

suppose I have a trivial type definition for redux-thunk:

declare module "redux-thunk" {
  import { Middleware } from 'redux';
  const thunkMiddleware: Middleware;
  export default thunkMiddleware;
}

And I use it in my app to create a store like:

import { Reducer, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const myReducer: Reducer<number> = (count, action) => count;

const myStore = createStore(myReducer, applyMiddleware(thunk));

I can't dispatch a redux-thunk function:

// fails with: Argument of type '(dispatch: any) => number' is not assignable to parameter of type 'Action'.
myStore.dispatch(dispatch =>
  setTimeout(() =>
    dispatch({
      type: "INCREMENT_COUNT"
    })
  , 500)
)

This is a complex api to account for in a type system, since the signature of dispatch changes depending on what middleware is loaded:

<A extends Action> dispatch(action: A)

becomes:

dispatch(action: Action | ThunkAction)

(where ThunkAction is (Dispatch, ()=>S) => void from above.

I'm not really sure immediately how to address this, but I was just wondering if there is an intended way to work with redux middleware.

cc'ing @aikoven @Igorbek @use-strict

Most helpful comment

@aikoven, please give explicit instructions. I think I've put enough time into trying to fill in the blanks. I'm not making any progress. Given redux supplies typings and refers to redux-thunk in the documentation, surely _someone_ knows how to make the two play nicely together under TypeScript.

As proof of effort, let's start with the obvious:

  • npm install redux
  • typings install npm~redux-thunk
import thunkMiddleware from 'redux-thunk';
console.log(thunkMiddleware)

Result: error TS2304: Cannot find name 'thunkMiddleware' in line 2. Looking in typings/modules/redux-thunk/index.d.ts, it turns out the source is andrew-w-ross/typings-redux, which the author deprecated in favour of the official redux typings. Which, sadly, don't include redux-thunk. Perhaps the other typings search redux-thunk result works:

  • typings remove redux-thunk
  • typings install --global dt~redux-thunk

This one comes from gaearon/redux-thunk via DefinitelyTyped. Success?

typings/globals/redux-thunk/index.d.ts(4,36): error TS2503: Cannot find namespace 'Redux'.
typings/globals/redux-thunk/index.d.ts(6,21): error TS2503: Cannot find namespace 'Redux'.

Aah, it'll only work if you use the global DefinitelyTyped typings for redux. Damn.

@HenrikBechmann offers, in DefinitelyTyped/DefinitelyTyped#6231:

I replaced the tsd-installed definition with this:

declare module 'redux-thunk' {
    import {MiddlewareArg} from 'redux'
    export default function(obj: MiddlewareArg): Function
}

Oh, that'll be easy to slot in, we lie to ourselves after the misadventures to date.

  • typings remove --global redux-thunk
  • cat > typings-local/redux-thunk.d.ts

More misadventures ensue, their shape depending on the angle of the attempt. This one produces a mysteriously hidden middleware function:

declare module "redux-thunk" {
    import { Middleware } from 'redux'
    export default Middleware
}

This is better. Note I'm not so much _programming_ by this point as _word processing_:

declare module "redux-thunk" {
    import { Middleware } from 'redux'
    const thunkMiddleware: Middleware
    export default thunkMiddleware
}

My success that it compiles is short lived, as I then try to _use_ the module:

    const thunkAction = getVersionFromServer(fetch);
    store.dispatch(thunkAction);

The compiler complains:

error TS2345: Argument of type '(dispatch: any) => any' is not assignable to parameter of type 'Action'.
  Property 'type' is missing in type '(dispatch: any) => any'.

Aha! @aikoven said all we need to do, _basically_, is augment the interface!

declare module 'redux' {
    export interface Dispatch<S> {
        <R>(thunkAction: (dispatch: Dispatch<S>) => R): R;
    }
}

That doesn't so much 'augment' it, as entirely mask the official typings:

typings-local/redux-thunk.d.ts(8,14): error TS2305: Module ''redux'' has no exported member 'Middleware'.

There's hours more of this (anyone else best mates with invalid module name in augmentation?)… but eventually you get to the point where it's obvious TypeScript's type system is _not_ saving time. Quite the opposite. So, I hack around it:

    const thunkAction = getVersionFromServer(fetch);
    store.dispatch(thunkAction as any as { type: string });

Slow clap, everyone. Oh, the Java guys will totally leave us alone, now.

You said it's basic, @aikoven. How about writing it down, eh?

All 7 comments

@pfgray See https://github.com/reactjs/redux/pull/1537
Basically you just augment Dispatch interface whenever middleware adds support for dispatching new type of things.

@aikoven, please give explicit instructions. I think I've put enough time into trying to fill in the blanks. I'm not making any progress. Given redux supplies typings and refers to redux-thunk in the documentation, surely _someone_ knows how to make the two play nicely together under TypeScript.

As proof of effort, let's start with the obvious:

  • npm install redux
  • typings install npm~redux-thunk
import thunkMiddleware from 'redux-thunk';
console.log(thunkMiddleware)

Result: error TS2304: Cannot find name 'thunkMiddleware' in line 2. Looking in typings/modules/redux-thunk/index.d.ts, it turns out the source is andrew-w-ross/typings-redux, which the author deprecated in favour of the official redux typings. Which, sadly, don't include redux-thunk. Perhaps the other typings search redux-thunk result works:

  • typings remove redux-thunk
  • typings install --global dt~redux-thunk

This one comes from gaearon/redux-thunk via DefinitelyTyped. Success?

typings/globals/redux-thunk/index.d.ts(4,36): error TS2503: Cannot find namespace 'Redux'.
typings/globals/redux-thunk/index.d.ts(6,21): error TS2503: Cannot find namespace 'Redux'.

Aah, it'll only work if you use the global DefinitelyTyped typings for redux. Damn.

@HenrikBechmann offers, in DefinitelyTyped/DefinitelyTyped#6231:

I replaced the tsd-installed definition with this:

declare module 'redux-thunk' {
    import {MiddlewareArg} from 'redux'
    export default function(obj: MiddlewareArg): Function
}

Oh, that'll be easy to slot in, we lie to ourselves after the misadventures to date.

  • typings remove --global redux-thunk
  • cat > typings-local/redux-thunk.d.ts

More misadventures ensue, their shape depending on the angle of the attempt. This one produces a mysteriously hidden middleware function:

declare module "redux-thunk" {
    import { Middleware } from 'redux'
    export default Middleware
}

This is better. Note I'm not so much _programming_ by this point as _word processing_:

declare module "redux-thunk" {
    import { Middleware } from 'redux'
    const thunkMiddleware: Middleware
    export default thunkMiddleware
}

My success that it compiles is short lived, as I then try to _use_ the module:

    const thunkAction = getVersionFromServer(fetch);
    store.dispatch(thunkAction);

The compiler complains:

error TS2345: Argument of type '(dispatch: any) => any' is not assignable to parameter of type 'Action'.
  Property 'type' is missing in type '(dispatch: any) => any'.

Aha! @aikoven said all we need to do, _basically_, is augment the interface!

declare module 'redux' {
    export interface Dispatch<S> {
        <R>(thunkAction: (dispatch: Dispatch<S>) => R): R;
    }
}

That doesn't so much 'augment' it, as entirely mask the official typings:

typings-local/redux-thunk.d.ts(8,14): error TS2305: Module ''redux'' has no exported member 'Middleware'.

There's hours more of this (anyone else best mates with invalid module name in augmentation?)… but eventually you get to the point where it's obvious TypeScript's type system is _not_ saving time. Quite the opposite. So, I hack around it:

    const thunkAction = getVersionFromServer(fetch);
    store.dispatch(thunkAction as any as { type: string });

Slow clap, everyone. Oh, the Java guys will totally leave us alone, now.

You said it's basic, @aikoven. How about writing it down, eh?

That is the definition of thunk's dispatch:

declare module "redux" {
    export interface Dispatch<S> {
        <R>(asyncAction: (dispatch: Dispatch<S>, getState: () => S) => R): R;
    }
}

Let me try to reproduce your issues.

@garthk I'm sorry if my words offended you. I'll add comments to the definition file with clarification on how new dispatch signatures are meant to be added by middleware typings.

No worries, @aikoven: you weren't offensive. I'm sorry I got stroppy. I tried to edit it down, but must have failed.

If you could document how to add new dispatch signatures, though, I'd find it very handy.

So, this looks to be more on the side of the middleware libraries. It looks like redux-thunk got typings added. Other libraries that need typings should have issues/PRs opened up on their respective repos. Thanks for the investigation on this!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

timdorr picture timdorr  Â·  3Comments

elado picture elado  Â·  3Comments

olalonde picture olalonde  Â·  3Comments

captbaritone picture captbaritone  Â·  3Comments

cloudfroster picture cloudfroster  Â·  3Comments