How to dispatch an Action or a ThunkAction (in TypeScript, with redux-thunk)?
Asked Answered
L

6

19

Say I have code like so:

import { Action, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';

interface StateTree {
  field: string;
}

function myFunc(action: Action | ThunkAction<void, StateTree, void>,
                dispatch: Dispatch<StateTree>) {
  dispatch(action); // <-- This is where the error comes from
}

...I get this error from the TypeScript compiler:

ERROR in myFile.ts:x:y
TS2345: Argument of type 'Action | ThunkAction<void, StateTree, void>' is not assignable to parameter of type 'Action'.
  Type 'ThunkAction<void, StateTree, void>' is not assignable to type 'Action'.
  Property 'type' is missing in type 'ThunkAction<void, StateTree, void>'.

I believe the problem is because of the way the redux-thunk type definition file augments the redux Dispatch interface and the inability for TypeScript to know which definition of Dispatch to use.

Is there a way around this?

Ladysmith answered 25/3, 2017 at 6:15 Comment(0)
D
4

I think you are correct in that despite being able to handle both types, typescript cannot work out which overload to use.

I think the best option for you is to cast back to the desired type when calling dispatch

function myFunc(action: Action | ThunkAction<void, StateTree, void>, 
                dispatch: Dispatch<StateTree>) {
  if (action instanceof ThunkAction<void, StateTree, void>) {
    dispatch(action as ThunkAction<void, StateTree, void>);
  } else {
    dispatch(action as Action);
  }
}

I hope I'm wrong and there is a better way to achieve this.

Daube answered 26/3, 2017 at 10:50 Comment(0)
C
28

ThunkAction signature changed with latest version (now is ThunkAction<void, Your.Store.Definition, void, AnyAction>) and unless some evil double casting (action as {} as Action), the more elegant way I found is to define the redux dispatch as a ThunkDispatch like this:

import { applyMiddleware, Store, createStore, AnyAction } from 'redux';
import logger from 'redux-logger';
import thunk, { ThunkDispatch } from 'redux-thunk';

import { Redux } from '../definitions';
import rootReducer from './reducers';
import { bootstrap } from './actions';

export default function configureStore() {

    const middleware = applyMiddleware( thunk, logger );

    const store: Store<Redux.Store.Definition> = createStore(rootReducer, middleware);

    // Here the dispatch casting:
    (store.dispatch as ThunkDispatch<Redux.Store.Definition, void, AnyAction>)( bootstrap() );

    return store;

}

In case someone else is looking for an updated answer! ^^

Choli answered 7/6, 2018 at 12:43 Comment(0)
Y
25

Time has passed and many things have changed since this question and various answers were posted. However, I found that none of the answers seemed satisfactory to me because the first two (michael-peyper and pierpytom) involved recasting/redefining which felt weird. The third (joaoguerravieira) seemed better since it didn't involve either of those, but it was unclear, to me at least, how it solved the problem.

This is what seemed most helpful to me when I ran into a problem which I think was practically identical to this: how to get a properly typed "dispatch" method on the created redux store. That is, how to get the TypeScript compiler to agree that store.dispatch could dispatch either Actions or ThunkActions. Even if this is not exactly the same problem being asked about in the original question (but I think it might be), all search engine queries about my problem kept leading me back to this post so I thought it might be helpful to put my solution here.

I have always found it super difficult to find the right types to use for things when using redux (maybe I'm just dumb) so for a long time I always just created my store like this:

createStore(
    combineReducers(stuff),
    defaultState,
    applyMiddleware(thunkMiddleware));

...which always put me in the situation where I could call store.dispatch on thunks but the TypeScript compiler yelled at me even though it would still work at runtime. Bits of each answer finally lead me to what I believe is the most up-to-date and no-casting way of solving the problem.

The typing of the dispatch method on the store object is dictated by what the call to redux's createStore returns. In order to have the right type on the store's dispatch method, you have to set the type parameters correctly on the call to applyMiddleware (which you either directly or eventually pass as the third parameter to createStore). @joaoguerravieira's answer led me to look in this direction. In order to get the dispatch method to have the right type to dispatch either ThunkAction or Action, I had to call createStore/applyMiddleware like this:

createStore(
    combineReducers(stuff),
    defaultState,
    applyMiddleware<DispatchFunctionType, StateType>(thunkMiddleware));

where

type DispatchFunctionType = ThunkDispatch<StateType, undefined, AnyAction>

By doing this, I got a store of this type:

Store<StateType, Action<StateType>> & { dispatch: DispatchFunctionType };

...which gets me a store.dispatch function of this type:

Dispatch<Action<any>> & ThunkDispatch<any, undefined, AnyAction>

...which can successfully dispatch an Action or a ThunkAction without yelling about type and without any redefinitions/casting.

Properly setting the type parameters on the call to applyMiddleware is critical!

Yeta answered 18/7, 2019 at 20:34 Comment(4)
Great answer. I had one other issue, cause I'm using 'composeWithDevTools(applyMiddleware(thunk, logger))'. So I just wrote my own interface 'export interface ThunkDispatchProp { dispatch: Dispatch<Action> & DispatchFunctionType }'. Then my components Prop interface doesn't extend 'DispatchProp', but 'ThunkDispatchProp'. Still feels like a hack a bit, but it works and if the next person looks at it he has a chance to figure out what's going on. Would be happy about your comment on it though.Elidiaelie
And if you move the call to combineReducers out of this call (const rootReducer = combineReducers(stuff)), then you can find StateType as it is the return type of the rootReducer (type StateType = ResultType<typeof rootReducer>).Christinachristine
What is ResultType? Ah, I think you meant ReturnType, ok.Preamplifier
From what I understand, joaoguerravieira's answer solves the problem by properly typing thunk. It's typed as ThunkMiddleware<S = {}, … and so by typing thunk as ThunkMiddleware<YourAppState> we give it the proper application state type, instead of just {}. Then the compiler is able to do the proper type detections.Koziol
H
10

These are the correct typings: https://github.com/reduxjs/redux-thunk/blob/master/test/typescript.ts

Most notably:

const store = createStore(fakeReducer, applyMiddleware(thunk as ThunkMiddleware<State, Actions>));

applyMiddleware will already override the dispatch with a ThunkDispatch.

Hoopoe answered 2/7, 2018 at 11:6 Comment(0)
D
4

I think you are correct in that despite being able to handle both types, typescript cannot work out which overload to use.

I think the best option for you is to cast back to the desired type when calling dispatch

function myFunc(action: Action | ThunkAction<void, StateTree, void>, 
                dispatch: Dispatch<StateTree>) {
  if (action instanceof ThunkAction<void, StateTree, void>) {
    dispatch(action as ThunkAction<void, StateTree, void>);
  } else {
    dispatch(action as Action);
  }
}

I hope I'm wrong and there is a better way to achieve this.

Daube answered 26/3, 2017 at 10:50 Comment(0)
P
3

If you're using Redux Toolkit's configureStore() abstraction, you can use the provided getDefaultMiddleware() callback that will provide you thunk middleware and a store.dispatch that can accept AnyAction or ThunkAction:

const store = configureStore({
  devTools: process.env.NODE_ENV !== 'production',
  // getDefaultMiddleware() adds redux-thunk by default
  // It also provides a properly typed store.dispatch that can accept AnyAction and ThunkAction
  // See https://redux-toolkit.js.org/api/getDefaultMiddleware
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(anyOtherMiddleware),
  preloadedState: {},
  reducer: reducers,
});

You can read more in their docs about configureStore() and getDefaultMiddleware().

Puebla answered 29/9, 2021 at 15:56 Comment(0)
H
0

maybe I'm late, but in my case, I can get it by defining the type of dispatch function explicitly

const rootReducer = combineReducers({......})

type DispatchFunctionType = ThunkDispatch<RootState, undefined, AnyAction>

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware<DispatchFunctionType, RootState>(thunk)));
export type RootState = ReturnType<typeof rootReducer>;

export default store;
Hankhanke answered 23/7, 2023 at 7:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.