Correct TypeScript Type for thunk dispatch?

Viewed 10973

I have an asynchronous redux action and so am using the thunk middleware.

I have mapStateToProps, mapDispatchToProps and connect functions for a component as follows:

const mapStateToProps = (store: IApplicationState) => {
  return {
    loading: store.products.loading,
    products: store.products.products
  };
};
const mapDispatchToProps = (dispatch: any) => {
  return {
    getAllProducts: () => dispatch(getAllProducts())
  };
};
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ProductsPage);

This all works but I wondered if is possible to replace the any type on the dispatch parameter in mapDispatchToProps?

I tried ThunkDispatch<IApplicationState, void, Action> but get the following TypeScript error on the connect function:

Argument of type 'typeof ProductsPage' is not assignable to parameter of type 'ComponentType<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>>'.
  Type 'typeof ProductsPage' is not assignable to type 'ComponentClass<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>, any>'.
    Types of property 'getDerivedStateFromProps' are incompatible.
      Type '(props: IProps, state: IState) => { products: IProduct[]; search: string; }' is not assignable to type 'GetDerivedStateFromProps<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>, any>'.
        Types of parameters 'props' and 'nextProps' are incompatible.
          Type 'Readonly<Matching<{ loading: boolean; products: IProduct[]; } & { getAllProducts: () => Promise<void>; }, IProps>>' is not assignable to type 'IProps'.
            Types of property 'getAllProducts' are incompatible.
              Type '() => Promise<void>' is not assignable to type '() => (dispatch: Dispatch<AnyAction>) => Promise<void>'.
                Type 'Promise<void>' is not assignable to type '(dispatch: Dispatch<AnyAction>) => Promise<void>'.
                  Type 'Promise<void>' provides no match for the signature '(dispatch: Dispatch<AnyAction>): Promise<void>'.

enter image description here

Is it possible to replace the any type on the dispatch parameter in mapDispatchToProps?

4 Answers

This setup works very well for me:

// store.ts
//...
export type TAppState = ReturnType<typeof rootReducer>;
export type TDispatch = ThunkDispatch<TAppState, void, AnyAction>;
export type TStore = Store<TAppState, AnyAction> & { dispatch: TDispatch };
export type TGetState = () => TAppState;
//...
const store: TStore = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(...middleware), ...enhancers)
);
export default store;

Where rootReducer on my setup looks like this const rootReducer = createRootReducer(history);

// createRootReducer.ts
import { combineReducers, Reducer, AnyAction } from 'redux';
import { History } from 'history';
import {
  connectRouter,
  RouterState,
  LocationChangeAction,
} from 'connected-react-router';
// ... here imports of reducers
type TAction = AnyAction & LocationChangeAction<any>;

export type TRootReducer = Reducer<
  {
    // ... here types of the reducer data slices
    router: RouterState;
  },
  TAction
>;
const createRootReducer = (history: History): TRootReducer =>
  combineReducers({
    // ... here actual inserting imported reducers
    router: connectRouter(history),
  });

export default createRootReducer;

Then in connected component

import { connect } from 'react-redux';
import Add, { IProps } from './Add'; // Component
import { TDispatch, TAppState } from '../../store';

type TStateProps = Pick<
  IProps,
  'title' | 'data' | 'loading'
>;
const mapStateToProps = (
  state: TAppState,
): TStateProps => {
    // ... here you have typed state :)
    // and return the TStateProps object as required
    return {
      loading: state.someReducer.loading,
      //...
    }
}

type TDispatchProps = Pick<IProps,  'onSave'>;
const mapDispatchToProps = (
  dispatch: TDispatch,
): TDispatchProps => {
   // here you  have typed dispatch now
   // return the TDispatchProps object as required
   return {
    onSave: (): void => {
      dispatch(saveEntry()).then(() => {
        backButton();
      });
    },
   }
}

As for the thunk actions I do as follows

// this is a return type of the thunk
type TPromise = Promise<ISaveTaskResponse | Error>;

export const saveEntry = (): ThunkAction<
  TPromise, // thunk return type
  TAppState, // state type
  any, // extra argument, (not used)
  ISaveEntryAction // action type
> => (dispatch: TDispatch, getState: TGetState): TPromise => {
  // use getState
  // dispatch start saveEntry action
  // make async call
  return Axios.post('/some/endpoint', {}, {})
    .then((results: { data: ISaveTaskResponse; }): Promise<ISaveTaskResponse> => {
      // get results
      // you can dispatch finish saveEntry action
      return Promise.resolve(data);
    })
    .catch((error: Error): Promise<Error> => {
      // you can dispatch an error saveEntry action
      return Promise.reject(error);
    });
};

Redux can dispatch actions that are plain objects. Say we have such action {type: 'ACTION2'}. We can create action creator and wrap it inside dispatch like this

// This is our action
interface Action2 extends Action { type: "ACTION2"; }

// And this is action crator
const action2 = (): Action2 => ({ type: "ACTION2" });

With thunk middleware Redux can dispatch functions. And we can create async action like this

// This action will be dispatched from async action creator
interface Action1 extends Action { type: "ACTION1"; payload: string; }

// And async action creator
const thunkAction = (arg: string): ThunkAction<Promise<void>, {}, AppState, KnownActions> => 
    async dispatch => {
        const res = await asyncFunction(arg);
        dispatch({ type: "ACTION1", payload: res });
    },  

Here ThunkAction<R, S, E, A extends Action> type is used. It accepts following type arguments:

  • R stays for return type of the internal function. In the above example, internal function is async function so it returns Promise<void>
  • S stays for the app state
  • E is the extension attribute type which is not used
  • A is type of the action. KnownActions in example above is the union of all possible action types (type KnownActions = Action1 | Action2;)

Now lets look at how component can be typed.

interface Component1DispatchProps {
  action2: () => Action2;
  thunkAction: (arg: string) => Promise<void>;
}

interface Component1StateProps {
  stateprop: string;
}

const mapDispatchToProps = (
  dispatch: ThunkDispatch<AppState, {}, KnownActions>
) => {
  return {
    action2: () => dispatch(action2()),
    thunkAction: (arg: string) => 
      dispatch(thunkAction(arg))
  };
};

thunkAction method returns dispatch(thunkAction(arg)), where thunkAction(arg) returns ThunkAction<Promise<void>, {}, AppState, KnownActions>. So dispatch will be called with the argument of type ThunkAction and will be resolved to

<TReturnType>(
  thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, TBasicAction>,
): TReturnType;

As a result dispatch(thunkAction(arg)) will return TReturnType or Promise<void> in our example. Such signature is set for thunkAction in Component1DispatchProps.

Complete example is here

In my experience the problem is not with the type of the dispatch parameter but actually the props type on the component.

Note: My solution is not using the function notation of mapDispatchToProps since that is not recommended until necessary (and I did not needed it yet). But I guess this solution could be adapted to it (having the right sandbox).

To define the props properly (without needing to create a copy-pasta interface) it is necessary to separate the mapDispatchToProps into two pieces, one for reducer actions and one for thunk actions.

To show this solution I've forked an example from another answer (thanks @Fyodor).

Full code here: https://codesandbox.io/s/thunkdispatch-forked-uss1b?file=/src/Component.tsx

Component props are a combination of:

interface ComponentOwnProps {
  ownprop: string;
}

const mapStateToProps = (state: AppState) => ({
  stateprop: state.stateprop
});

const mapDispatchToProps = {
  action2
};

const mapDispatchThunkToProps = {
  thunkAction
};

The complete type of the props is defined as:

type ComponentProps = ComponentOwnProps &
  ReturnType<typeof mapStateToProps> &
  typeof mapDispatchToProps &
  ThunkProps<typeof mapDispatchThunkToProps>;

The only trick here is ThunkProps which converts the Thunk action types in the dictionary from "double function" (args) => (dispatch, getState) => void to the connected (and simpler) (args) => void functions.

// types from redux examples (usually in `store.ts`)
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  any, // or some ThunkExtraArgument interface
  Action<string>
>;

// the tricky part: convert each `(args) => AppThunk => void` to `(args) => void`
export type ThunkProps<T extends { [K in keyof T]: (...a: any[]) => AppThunk<void> }> = {
  [K in keyof T]: (...args: Parameters<T[K]>) => void;
};

... and finally you can connect the component

// don't forget to combine the two mapDispatch-es
export default connect(
  mapStateToProps,
  {...mapDispatchToProps, ...mapDispatchThunkToProps}
)(Component);

Full code (forked and updated from @Fyodor's answer): https://codesandbox.io/s/thunkdispatch-forked-uss1b?file=/src/Component.tsx ... all important code and types are in Component.tsx for convenience.

Note: even though this solution has some limitations, for me it serves the purpose of not writing unnecessary interfaces and having well typed props including function params.

You could also provide another override for the redux Dispatch function in your global type definition:

(put this inside global.d.ts or some other global type definition)

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

declare module 'redux' {
    export interface Dispatch<A extends Action = AnyAction> {
        <T extends ThunkAction<any, any, any, any>>(action: T): T extends ThunkAction<infer K, any, any, any> ? K : never;
    }
}

And then in your mapDispatchToProps use Dispatch type and not ThunkDispatch.

Related