Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example with Typescript, react-redux, and redux-thunk #213

Closed
jfbloom22 opened this issue Aug 17, 2018 · 20 comments · Fixed by #224
Closed

Example with Typescript, react-redux, and redux-thunk #213

jfbloom22 opened this issue Aug 17, 2018 · 20 comments · Fixed by #224

Comments

@jfbloom22
Copy link

So glad to see redux-thunk has great support for Typescript and Redux v4. Where is the best place to view an up to date example of a correctly typed reducer, actions, and a connected component with redux-thunk? This test is a good start: https://github.com/reduxjs/redux-thunk/blob/master/test/typescript.ts
But I can not seem to find an up to date example of what a correctly typed connected component should look like. Specifically I am looking for something that shows mapStateToProps, mapDispatchToProps, and the props interface for the component.

@timdorr
Copy link
Member

timdorr commented Aug 17, 2018

We're planning on moving the types out of the package and into DefinitelyTyped, so this will be on the community to provide. For now, the tests will be helpful.

@timdorr timdorr closed this as completed Aug 17, 2018
@jfbloom22
Copy link
Author

jfbloom22 commented Aug 17, 2018

@timdorr sounds great and I am looking forward to it. If the solution happens to be easy for you, the main thing I am stuck on is how to return a promise in the action.

type ThunkResult<R> = ThunkAction<R, IinitialState, undefined, any>;

export function anotherThunkAction(): ThunkResult<Promise<boolean>> {
  return (dispatch, getState) => {
    return Promise.resolve(true);
  }
}

then in my component I have a prop interface:

interface IProps {
  anotherThunkAction: typeof anotherThunkAction;
}

Then:

  componentWillMount() {
    this.props.anotherThunkAction().then(() => {console.log('hello world')})
  }

I am getting this typescript error: Property 'then' does not exist on type 'ThunkAction<Promise<boolean>, IinitialState, undefined, any>'.

FYI I am going to post this question to stack overflow as well.

@jfbloom22
Copy link
Author

asked this on Stack overflow, but no luck so far. https://stackoverflow.com/questions/51898937/returning-a-promise-in-a-redux-thunk-with-typescript

@timdorr I know you plan to move the typing to Definitely Typed, but perhaps including an Async function in the typescript.ts tests is a good idea?

@mario-jerkovic
Copy link

@jfbloom22 Did you maybe found a solution?

@jfbloom22
Copy link
Author

@mario-jerkovic no, I still have not found a solution. And I have not received any answers on stack overflow either. It seems strange to me that more people are not using redux-thunk with promises and typescript.
I am so disappointed by this I am considering switching away from redux-thunk to redux-saga or redux-observable. Both of those appear to have very good typescript support.

@zypher2004
Copy link

@jfbloom22 Any luck figuring this out yet or did you end up switching away from redux-thunk?

@laat
Copy link
Contributor

laat commented Oct 9, 2018

This should work:

import { ThunkAction, ThunkDispatch } from 'redux-thunk';
import React from 'react';
import { connect } from 'react-redux';
import { Action } from 'redux';

type MyRootState = {};
type MyExtraArg = undefined;
type MyThunkResult<R> = ThunkAction<R, MyRootState, MyExtraArg, Action>;
// Next Line:
// It is important to use Action as last type argument, does not work with any.
type MyThunkDispatch = ThunkDispatch<MyRootState, MyExtraArg, Action>;

const anotherThunkAction = (): MyThunkResult<Promise<boolean>> => (dispatch, getState) => {
  return Promise.resolve(true);
};

export interface IProps {
  anotherThunkAction: () => Promise<boolean>;
}

export class Foo extends React.Component<IProps> {
  componentDidMount() {
    this.props.anotherThunkAction().then(value => {
      console.log('hello world, got', value);
    });
  }
  render() {
    return null;
  }
}

const mapDispatchToProps = (dispatch: MyThunkDispatch) => ({
  anotherThunkAction: () => dispatch(anotherThunkAction()),
});

export default connect(
  () => undefined,
  mapDispatchToProps
)(Foo);

the action type argument can also be like the test.

type Actions = { type: 'FOO' } | { type: 'BAR', result: number };


Sidenote:
By swapping the calls here, we can use any as the action type. I'm creating a PR for that.

<T extends A>(action: T): T;

@jfbloom22
Copy link
Author

I was able to get this to work with @laat 's suggestion above. Thanks! But good grief this is a lot of typing. In my original question I was using "typeof" in my "Iprops" interface. This allowed me to add types to the params in my action function a single time and get the benefit of autocomplete params throughout my project.

With this implementation, I am adding types to the params 3 times. Once in the original function, once in "Iprops", and then again in "mapDispatchToProps". Is this the best we can do?

To Illustrate where I am adding types to the params 3 times:

export function testingPromise(message: string): ThunkResult<Promise<string>> {
  return (dispatch, getState) => {
    return Promise.resolve(message)
  }
}
interface IProps {
testingPromise: (message: string) => Promise<string>;
}
const mapDispatchToProps = (dispatch: MyThunkDispatch) => ({
testingPromise: (message: string) => dispatch(testingPromise(message))
})

@laat
Copy link
Contributor

laat commented Mar 16, 2019

We can reduce the duplication a bit, with new code in the master branch.

But until @timdorr releases a new version with the types from #224, we still have to add type-definitions ourself:

src/types/redux-thunk-pr224.d.ts

// Remove when https://github.com/reduxjs/redux-thunk/pull/224 is released.
import { Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
declare module "redux" {
  function bindActionCreators<M extends ActionCreatorsMapObject<any>>(
    actionCreators: M,
    dispatch: Dispatch
  ): {
    [N in keyof M]: ReturnType<M[N]> extends ThunkAction<any, any, any, any>
      ? (...args: Parameters<M[N]>) => ReturnType<ReturnType<M[N]>>
      : M[N]
  };
}

then we can write mapDispatchToProps from the previous solution like so:

import { bindActionCreators } from "redux";

const mapDispatchToProps = (dispatch: MyDispatch) =>
  bindActionCreators({ anotherThunkAction }, dispatch);

The duplication of types in IProps is IMHO desired. It makes the component reusable in any context whether it is redux, react-apollo or hooks 🎉.

If you are unconcerned about coupling your components to thunks, it is possible with a utility type:

type BoundThunk<
  T extends (...args: any[]) => ThunkAction<any, any, any, any>
> = (...args: Parameters<T>) => ReturnType<ReturnType<T>>;

export interface IProps {
  // tight coupling
  anotherThunkAction: BoundThunk<typeof anotherThunkAction>;
}

@jfbloom22
Copy link
Author

@laat this bindActionCreators() function and new types will be awesome. And thanks for the suggestion, I am going to think about how to decouple my components. In the meantime thanks for the utility type. This gives me something to use and something to study in order to upgrade my Typescript skills. It continues to be very rewarding as I learn more about Typescript and how to use it well.

@oisinlavery
Copy link

It's been some time, was this ever fixed?

@markerikson
Copy link
Contributor

I've got examples of working with Redux Toolkit, thunks, and React-Redux in the Redux Toolkit "Advanced Tutorial" docs page.

@Yan1
Copy link

Yan1 commented Dec 13, 2019

@jfbloom22 @oisinlavery Below works for me:

type BoundThunk<T extends (...args: any[]) => ThunkAction<any, any, any, any>> =
  (...args: Parameters<T>) => ReturnType<ReturnType<T>>;

type StateProps = ReturnType<typeof mapStateToProps>
type DispatchProps = { [P in keyof typeof mapDispatchToProps]: BoundThunk<typeof mapDispatchToProps[P]> }
type OwnProps = {}
type Props = StateProps & DispatchProps & OwnProps

@rossPatton
Copy link

will this ever be improved? as long as the solution to such a common problem remains this verbose (and difficult to find examples for, since most tutorials are overly simple counter or todo apps) I don't think many people are gonna actually do it

i'm having that debate myself right now, and frankly i'm leaning towards just clobbering anything to do with redux-thunk with any just to avoid the error spam

the effort to payoff ratio just isn't very good here

@markerikson
Copy link
Contributor

@rossPatton : as I linked a couple comments above, the Redux Toolkit "Advanced Tutorial" docs page shows a recommended approach to working with thunks in TS. We also have an example in the Redux docs "Usage with TypeScript" page.

@delewis13
Copy link

delewis13 commented Mar 19, 2020

@markerikson

(1) Is it possible to add a little more detail to the discussion of TS & thunks in the Redux Toolkit docs? Particularly in dealing with the typing inside mapDispatch and useDispatch. For example, in the docs it suggests: export type AppDispatch = typeof store.dispatch. If you were to use this as follows:

const mapDispatchToProps = (dispatch: AppDispatch) => ({
  asyncAction: () => dispatch(asyncAction())
  isAnAction: () => dispatch({ type: 'IS_AN_ACTION', value: 5})
  notAnAction: () => dispatch({type: 'NOT_AN_ACTION', invalidValue: 5})
})

While it would deal fine with isAnAction and notAnAction, it would also highlight asyncAction as being problematic, which it isn't. Seems as though it would be better to define:

export type AppDispatch = ThunkDispatch<MyRootState, any, MyUnionOfActions>

Which gives the correctly shows notAnAction as problematic, while letting the thunk'd action and valid action through.

(2) Furthermore, I can't for the life of me get properly typechecked actions when using redux-thunk & the object form of mapDispatchToProps [though the functional form works fine]. This seems problematic... Can see others also having this issue in bottom of comments @ https://gist.github.com/milankorsos/ffb9d32755db0304545f92b11f0e4beb

@laat

Thank you for your minimal example posted above! If I may suggest a small change, where you use Action in defining type MyThunkResult<R> and MyThunkDispatch, perhaps it might be more useful to show how you should use your own union of actions there, rather than the generic Action? For some this might be obvious, but for me that was the final piece to the puzzle.

const ACTION_ONE = 'ACTION_ONE'
const ACTION_TWO = 'ACTION_TWO'
myUnionOfActions =
  | { type: typeof ACTION_ONE; payload: number }
  | { type: typeof ACTION_TWO; payload: string }

type MyThunkResult<R> = ThunkAction<R, MyRootState, MyExtraArg, myUnionOfActions>;
type MyThunkDispatch = ThunkDispatch<MyRootState, MyExtraArg, myUnionOfActions>;

@markerikson
Copy link
Contributor

PRs to improve the docs are always appreciated, yeah.

Personally, I've never found restrictions on the type of dispatch to be particularly useful, but I understand others prefer that.

@tar-aldev
Copy link

For guys using hooks, this may be useful.

I struggled with proper types for useDispatch(). It seems like by default it has any type.
Therefore for redux-thunk action, it will not understand that promise was returned.

I am not 100% sure if this is correct solution and would greatly appreciate any comments / suggestions.

creating typings for actions

// undefined - extra arguments
// AuthActionTypes - action typings you have
// Check creating typings for actions above for details.
// Probably you would have all actions connected in rootActionTypes or something like that;

// ThunkDispatch did the trick for me. After it I got proper return type
const dispatch = useDispatch<ThunkDispatch<RootState, undefined, AuthActionTypes>>();

const {result} = await dispatch(someActionAsync());

In actions.ts

type ThunkResult<Result> = ThunkAction<
  Result,
  RootState,
  undefined,
  AuthActionTypes
>;

export const someActionAsync = (): ThunkResult<Promise<{ result: string }>> => {
  return async (
    dispatch: ThunkDispatch<
      RootState,
      undefined,
      AuthActionTypes
    >
  ) => {
    dispatch(signinWithGoogleStart(googleCode));

    try {
      const { data } = await someApiCall()
      dispatch(successAction());
      return { result: 'yeah it worked fine' };
    } catch (error) {
      if (error.response) {
        console.log('Network error', error.response);
      }
      console.log('error', error);
      dispatch(failureAction());
      return { result: 'yeah it worked fine' };
    }
  };
};

@delewis13
Copy link

delewis13 commented Mar 25, 2020

PRs to improve the docs are always appreciated, yeah.

Personally, I've never found restrictions on the type of dispatch to be particularly useful, but I understand others prefer that.

After thinking about it a little longer, as well as trying to get the 100% type-safety to work while using createSlice and failing to get the string literal type right, as documented, I think i've come around to your side.

I suppose that given you are always using action creators and they are properly typed, there is little to no need for type safety on the actual dispatch. The only way you could go wrong is by somehow passing in an argument to dispatch that conforms to the generic action expectations, but also isn't the argument you intended to pass in? Seems improbable...

Is that your line of thinking when you say you don't find it useful?

@markerikson
Copy link
Contributor

Mostly, yeah.

Also, the notion of limiting what can be dispatched is kind of faulty to begin with. Conceptually, dispatching actions is much like triggering an event emitter. You don't know for sure what callbacks may be registered for what actions, and it's legal to emit an event that has no listeners registered at all.

In the same way, it's entirely fine to dispatch an action that results in 0 state updates, because no reducers actually cared about it. This kind of goes along with our Style Guide recommendation to "model actions as events".

If you think of it that way, the notion of saying "this is an invalid action to dispatch" goes out the window. Any action is legal to dispatch, it's just a question of whether any reducer or middleware logic actually cares about that action.

There probably are still cases where a union of actions may be useful (I think redux-observable kind of relies on that for some of its operators), but I don't see it as a critical piece overall.

It's worth noting that my views on TypeScript are strongly pragmatic, not absolutist. I'm sold on using TS on every project I work on, and do want as much of my codebase to be typed as possible. At the same time, much like unit testing and code coverage, getting that last 20% covered takes significantly more time and effort than the first 80%, and the marginal benefit of doing so goes down considerably.

Because of that, I'm not worried about "trying to have every single line of my codebase 100% perfectly typed and preventing all impossible bits of behavior", as I've seen many people try to do. I'm fine with inserting some manual type declarations to bridge the gaps between pieces of code that aren't quite connected from the compiler's point of view, or strategically using any in key spots internally if it saves me a few hours of wrestling with the compiler over something that I know works fine going in and coming out.

So, for my own apps, I'd want to ensure that my reducers, components, thunks, and util functions are correctly typed, with any action creators deriving their types from the reducers (ie, createSlice). I can accomplish that straightforwardly just by declaring arguments and prop interfaces, and that to me hits the sweet spot in terms of benefit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.