Search code examples
reactjstypescriptreduxredux-thunk

Typescript apparently deduces the wrong type for a dispatched Thunk


Complete working codesandbox example here

I declare a simple action type and an asynchronous thunk that dispatches it:

type ActionType = { type: "foo"; v: number };

const FooThunk: ThunkAction<Promise<ActionType>, any, any, ActionType> = (dispatch): Promise<ActionType>
=> {
  return new Promise<number>((resolve) => {
    setTimeout(() => {
      resolve(42);
    }, 100);
  }).then((v: number) => {
    return dispatch({ type: "foo", v });
  });
};

The question is now what is the type of the value I get when I call dispatch(FooThunk). Typescript thinks the type is ThunkAction<Promise<ActionType>, any, any, ActionType> and complains (on line 38 of the sandbox) with the following message:

'ThunkAction<Promise<ActionType>, any, any, ActionType>' is missing the following properties from type 'Promise<ActionType>': then, catch, [Symbol.toStringTag]ts(2739)

However, when I log the value I get at runtime (line 48 of the codesandbox) I see clearly that it is a Promise. Searching on StackOverflow I have found contradictory answers. This answer says that dispatching a thunk returns the thunk itself. Whereas this answer suggests that dispatching a thunk returns a Promise.

Typescript's type system seems to say that the type of dispatching a thunk is the same as the thunk itself. However at runtime I get a Promise object. What am I missing?

For completeness purposes only, I append the code that you will find the the sandbox (link provided above):

import * as React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { initialState, rootReducer } from "./rootReducer";
import "./styles.css";

import { ThunkDispatch as Dispatch, ThunkAction } from "redux-thunk";
import { connect, ConnectedProps } from "react-redux";

import { applyMiddleware } from "redux";

import thunk from "redux-thunk";

const store = createStore(
  rootReducer /* preloadedState, */,
  applyMiddleware(thunk)
);

//const store = createStore(rootReducer, initialState);

type ActionType = { type: "foo"; v: number };

const FooThunk: ThunkAction<Promise<ActionType>, any, any, ActionType> = (
  dispatch
): Promise<ActionType> => {
  return new Promise<number>((resolve) => {
    setTimeout(() => {
      resolve(42);
    }, 100);
  }).then((v: number) => {
    return dispatch({ type: "foo", v });
  });
};

const mapDispatchToProps = (dispatch: Dispatch<any, any, any>) => {
  return {
    dispatchFooThunk: (): Promise<ActionType> => dispatch(FooThunk)
  };
};
const connector = connect(null, mapDispatchToProps);

type PropsFromRedux = ConnectedProps<typeof connector>;

class FooComponent_ extends React.Component<PropsFromRedux> {
  componentDidMount() {
    const p = this.props.dispatchFooThunk();
    console.log(p); // if you examine log output you see clearly that this is a PROMISE !
  }
  render() {
    return <div>foo</div>;
  }
}

const FooComponent = connector(FooComponent_);

class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <FooComponent />
      </Provider>
    );
  }
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

Solution

  • The return value of a thunk is the return value of the thunk action itself. As a consequence: If it's an async function, it returns a Promise.

    The normal Dispatch type does not know how to handle thunks though. If you are using redux toolkit this documentation part describes how to properly type your useDispatch hook with a Dispatch type inferred from the store configuration. If you are using plain redux, you just can use the ThunkDispatch type. This part of the react-redux documentation describes how to type your useDispatch hook in that case.

    PS: in your specific case, your ThunkDispatch<any, any, any> is too generic. That last any will match too eagerly. Try a ThunkDispatch<any, any, AnyAction> instead, that will work.