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

Add overload for bindActionCreators #224

Merged
merged 3 commits into from
Jan 29, 2019
Merged

Add overload for bindActionCreators #224

merged 3 commits into from
Jan 29, 2019

Conversation

RMHonor
Copy link
Contributor

@RMHonor RMHonor commented Nov 5, 2018

As per #223, redux's bindActionCreators only works nicely for standard action creators. This implementation infers the return type of ThunkActions so the application could respond accordingly. E.g. action fires an XHR, the application has no way of knowing when the XHR has resolved, other than through watching the state. This way, you can now have promise resolution in your application outside of the redux store.

I've updated the TypeScript version to 3.1, this gives us access to the Parameters generic, the ReturnType generic, and conditional types.

Feedback welcome

Note, the should fix the concerns from the now closed #213

@timdorr
Copy link
Member

timdorr commented Nov 5, 2018

I'm pretty sure this is fine. Any TS experts want to chime in?

@farnoy
Copy link

farnoy commented Nov 22, 2018

Am I approaching it wrong? I used your declare module 'redux' override, without switching to your branch. I had to do it like this:

interface OwnProps {
  a: number;
  b: string;
}

// my custom helper
type ConnectedThunk<T> = T extends (...args: infer K) => ThunkAction<infer R, infer _S, infer _Element, infer _A> ? (...args: K) => R : T;

type ConnectProps<T extends object> = {
  [P in keyof T]: ConnectedThunk<T[P]>
};

type DispatchProps = ConnectProps<{
  someAction: typeof actions.someAction, // ReduxThunk<...>
}>;

type Props = OwnProps & DispatchProps;

class Test extends React.Component<Props, {}> { ... }

const actions = (dispatch: Dispatch<AnyAction>) => bindActionCreators({
  someAction: actions.someAction,
}, dispatch);

export default connect<{}, DispatchProps, OwnProps, {}>(selector, actions)(Test);

Is there a way to avoid some type declarations and my ConnectProps transformation?

The above works very well so far, just a bit verbose and we may need to ship a helper like ConnectProps?

@RMHonor
Copy link
Contributor Author

RMHonor commented Nov 22, 2018

@farnoy Yes, you'll have to define your own return type if you don't pull it from the bindActionCreators return type. It's something I've built myself and didn't think to include inside redux-thunk, I'll add the base generic.

@farnoy
Copy link

farnoy commented Nov 22, 2018

I suppose that if you define your const actions = ... bindActionCreators(...) first, you can then use type Props = OwnProps & ReturnType<typeof actions>. I haven't checked this though.

@optimistiks
Copy link

I think there is something wrong with ThunkActionDispatch, or maybe it's not clear for me how to use it.

Suppose I have this ThunkAction definition in my app

export interface IThunkAction<R>
  extends ThunkAction<R, IRootState, IThunkExtraArgument, IRootAction> {}

Then I have my thunk action creator

const updateName: (name: string) => IThunkAction<Promise<void>> = (name) => {
    return async (dispatch, getState, extraArg) => { // do some stuff } 
}

At this point, the typing for updateName is correct. It accepts a string (name), and returns an IThunkAction.

Then I do

const mapDispatchToProps = (dispatch: Dispatch) =>
  bindActionCreators({ updateName }, dispatch);

At this point the typing is correct too. I can see that mapDispatchToProps is a function that returns an object that contains the updateName function that returns a Promise<void> instead of IThunkAction.

Now, I need to pass that mapDispatchToProps to the redux connect, and supply it with the DispatchProps definition.

interface DispatchProps {
    updateName: ???
}

updateName: ThunkActionDispatch<typeof updateName> does not work (because updateName is not a ThunkAction).

updateName: () => ThunkActionDispatch<ReturnType<typeof updateName>> does not work as well (and I have to re-type the name argument in this case, which I don't want to to - I already declared those arguments once).

The only thing that works is this:

interface DispatchProps extends ReturnType<typeof mapDispatchToProps> {}

Only in this case, I have the updateName function properly typed in my react component, I see what arguments it accepts, and I see that it returns a promise and not a thunk action.

@RMHonor
Copy link
Contributor Author

RMHonor commented Dec 3, 2018

@optimistiks Yes, you're right, I incorrectly typed ThunkActionDispatch, I copied it from a personal project which has a slightly implementation of inferring dispatch types for components. I've added some tests to verify that ThunkActionDispatch correctly mapped types as you'd expect from bindActionCreators

@C-Higgins
Copy link

@optimistiks I don't think you need to define the interface for DispatchProps at all.
The React component can take as its props definition ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps> and the connect can simply be connect(mapStateToProps, mapDispatchToProps) (with no type params) and all types are properly inferred, assuming the bindActionCreators type definition override from this pull request has been added.

At this point though I don't see what the purpose is of the ThunkActionDispatch type. @RMHonor Could you explain why or when this type would be needed?

@RMHonor
Copy link
Contributor Author

RMHonor commented Dec 4, 2018

@C-Higgins Some developers prefer to define what the component expects as actions, rather than the actions defining the props. Personally in future I think I'll take your strategy, it seems to be clearer and requires less in the way of interface definition. But for those who'd rather define props first, the option is there.

@ekilah
Copy link

ekilah commented Dec 12, 2018

chiming in to say that, as a non-expert user of redux and as someone attempting to add some redux-thunk actions in my TS project, it would be great if some extra documentation came with these changes, i.e. a full example with TS types that shows how to properly annotate things like mapDispatchToProps / bindActionCreators and our actionCreators/actions with types so the compiler is happy.

I believe this fix would help me, but I'm not sure it'd be obvious what the right types are for each piece w/o a nice example. links welcome if this already exists somewhere!

@markerikson
Copy link
Contributor

@ekilah : we do have a WIP docs PR to add a page on using TypeScript:

reduxjs/redux#3201

Having said that, Tim and I don't actively use TS, and neither of us know enough on the topic to write docs like this ourselves.

@iTakeshi
Copy link

Tried your branch in my personal project, and it perfectly worked. Thanks for this PR, and waiting for merge.

@malash
Copy link

malash commented Jan 15, 2019

I use these lines for store.dispatch(thunkActionHere()):

  interface Dispatch<A extends Action = AnyAction> {
    <T extends A>(action: T): T;
    <R, S, E, T extends A>(action: ThunkAction<R, S, E, T>): R;
  }

@ablakey
Copy link

ablakey commented Jan 16, 2019

Thanks for this. Using this branch in my code and it addresses the above issue. Here's an example of how I'm using it for others who come across this (because I always appreciate having lots of tangible examples).

import React from 'react';
import { bindActionCreators, AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {AppState} from './AppState'

// Types to cut down on boilerplate across dozens of thunks.
type Dispatcher = ThunkDispatch<AppState, undefined, AnyAction>;
type GetState = () => AppState;

const getAuthToken = (username: string, password: string) => {
  return async (dispatch: Dispatcher, getState: GetState) => {
    await request.post(`auth/url/`).send({ username, password });
    return dispatch({ type: 'SET_AUTH_TOKEN', token: res.body.token, username });
  };
};

// Before this PR, `bindActionCreators` would mess up the typing, causing an error below...
const mapDispatch = (dispatch: Dispatcher) => bindActionCreators({
  getAuthToken,
}, dispatch);

type Props = ReturnType<typeof mapDispatch>;

class MyComponent extends React.PureComponent<Props> {

  public render() {
    // Before this PR, the line below would complain that we can't await non-Promises.
    // the bindActionCreators was messing up the typing so it thought we were returning a
    // normal function, not an async function (I think).
    await this.props.getAuthToken('username', 'password');
    console.log('Then I do something else.');
  }
}


export default connect(null, mapDispatch)(MyComponent)

@RMHonor
Copy link
Contributor Author

RMHonor commented Jan 29, 2019

Is there any chance of getting some movement on this?

@timdorr
Copy link
Member

timdorr commented Jan 29, 2019

I haven't seen any unaddressed complaints, so I'll merge it in.

@bamaboy
Copy link

bamaboy commented Feb 13, 2019

Any idea when this would be released?

@laat
Copy link
Contributor

laat commented Mar 16, 2019

@markerikson @timdorr can we publish this change to npm?

@igozali
Copy link

igozali commented May 13, 2019

Just wondering what the plan is for publishing a new version of this to npm?

@bensaufley
Copy link

bensaufley commented Jun 21, 2019

Just encountered this issue myself; merged long ago, but still not on NPM. Also wondering why. But not just here for a +1, I'm also wondering if someone can tell me why this Dispatch isn't ThunkDispatch?

Additionally, this appears to create inconsistent behavior. When I pass in an object, my TS looks like this:

interface DispatchProps {
  myActionCreator: MyThunkActionCreator;
}

// Please don't read too much into this dummy component
const MyComponent: FunctionComponent<ResolveThunks<DispatchProps>> = ({ myActionCreator }) => (
  <h1>{myActionCreator()}</h1>
);

export default connect<{}, DispatchProps, {}, MyStore>(null, { myActionCreator })(MyComponent);

You can see that the connect expects DispatchProps, with the ThunkAction, and the connected component expects ResolveThunks<DispatchProps>, with the thunk resolved.

But when I use bindActionCreators, I'm required to ResolveThunks everywhere (or otherwise pass in the type of the resolved thunks myself):

interface DispatchProps {
  myActionCreator: MyThunkActionCreator;
}

// Please don't read too much into this dummy component
const MyComponent: FunctionComponent<ResolveThunks<DispatchProps>> = ({ myActionCreator }) => (
  <h1>{myActionCreator()}</h1>
);

const mapDispatchToProps: MapDispatchToPropsFunction<ResolveThunks<DispatchProps>, {}> = (dispatch) => bindActionCreators({ myActionCreator });

export default connect<{}, ResolveThunks<DispatchProps>, {}, MyStore>(null, mapDispatchToProps)(MyComponent);

@jamestharpe
Copy link

Checking in: https://www.npmjs.com/package/redux-thunk hasn't been updated in over a year and therefore doesn't include this fix.

Is there anything we can do to get this change published?

@DeyLak
Copy link

DeyLak commented Jul 23, 2019

+1 Any plans on release it?

@timdorr
Copy link
Member

timdorr commented Jul 23, 2019

I don't think the types on master are in a good spot just yet. I don't want to push out a bad release and upset folks even more.

@timini
Copy link

timini commented Jul 30, 2019

bindActionCreators typings still making my prefrontal cortex bleed

@chrispaynter
Copy link

chrispaynter commented Aug 7, 2019

@timini, for me to be able to forget about this problem till it's released, I just created a typing file such as /typings/redux-thunk/index.d.ts, and then used the below typings. Only time I remember there's an issue now is when I see the notifications here on this Github issue :D

Disclaimer - I can't remember where I got these definitions from, but it'll be a simple case of deleting them when the release happens.

I'm also not aware of any side effects it may have, but I've not experienced any yet.

import {
  Action,
  ActionCreatorsMapObject,
  AnyAction,
  Dispatch,
  Middleware
} from 'redux';

export interface ThunkDispatch<S, E, A extends Action> {
  <R>(thunkAction: ThunkAction<R, S, E, A>): R;
  <T extends A>(action: T): T;
}

export type ThunkAction<R, S, E, A extends Action> = (
  dispatch: ThunkDispatch<S, E, A>,
  getState: () => S,
  extraArgument: E
) => R;

/**
 * Takes a ThunkAction and returns a function signature which matches how it would appear when processed using
 * bindActionCreators
 *
 * @template T ThunkAction to be wrapped
 */
export type ThunkActionDispatch<
  T extends (...args: any[]) => ThunkAction<any, any, any, any>
> = (...args: Parameters<T>) => ReturnType<ReturnType<T>>;

export type ThunkMiddleware<
  S = {},
  A extends Action = AnyAction,
  E = undefined
> = Middleware<ThunkDispatch<S, E, A>, S, ThunkDispatch<S, E, A>>;

declare const thunk: ThunkMiddleware & {
  withExtraArgument<E>(extraArgument: E): ThunkMiddleware<{}, AnyAction, E>;
};

export default thunk;

/**
 * Redux behaviour changed by middleware, so overloads here
 */
declare module 'redux' {
  /**
   * Overload for bindActionCreators redux function, returns expects responses
   * from thunk actions
   */
  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]
  };
}

@29x10

This comment has been minimized.

@krailler

This comment has been minimized.

@smithki

This comment has been minimized.

@reduxjs reduxjs deleted a comment from jamestharpe Aug 10, 2020
@andradaelenabucur
Copy link

andradaelenabucur commented Aug 21, 2020

Hi @RMHonor,
I've managed to get the correct return types when using bindActionCreators with thunks. However, I'm stuck with thunk actions defined with generics.
If I have for example an action defined like this:

const actionExample = <T extends SomeInterface>(anyData: T) =>
async (dispatch, getState) => {
// do something with data here
},
when I use bindActionsCreators's return result, the type of actionExample will be (anyData: SomeInterface) => Promise<void> instead of <T extends SomeInterface>(anyData: T) => Promise<void>.
Has anyone run into this or am I doing something wrong?

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 this pull request may close these issues.

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