Definitelytyped: [@types/react-redux] 6.0.10 fails to resolve thunks

Created on 22 Nov 2018  路  13Comments  路  Source: DefinitelyTyped/DefinitelyTyped

  • [x] I tried using the @types/react-redux package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @surgeboris

Hi @surgeboris, I see that your recent changes intend is to "properly support thunks". However they seem to have had the exact opposite effect in our code base!

This code works in 6.0.9:

export const OrderForm = connect(
  (state: State) => ({
    order: getOrder(state),
    initialOrder: Order.createEmpty(),
    isConfirming: isConfirming(state),
    externalQuote: getExternalQuote(state),
    panelQuote: getPanelQuote(state),
  }),
  {
    onCancel: cancelOrder,
    fetchExternalQuote,
    fetchPanelQuote,
    fetchQuoteQueued,
    confirmOrder,
    createOrder,
  }

The new error in 6.0.10:

Argument of type 'typeof OrderForm' is not assignable to parameter of type 'ComponentType<Matching<{ order: Readonly<PermittedOrderParams>; initialOrder: PermittedOrderParams; isConfirming: boolean; externalQuote: Readonly<ThirdPartyQuote>; panelQuote: Readonly<PanelQuote>; } & { ...; }, Props>>'.
  Type 'typeof OrderForm' is not assignable to type 'ComponentClass<Matching<{ order: Readonly<PermittedOrderParams>; initialOrder: PermittedOrderParams; isConfirming: boolean; externalQuote: Readonly<ThirdPartyQuote>; panelQuote: Readonly<PanelQuote>; } & { ...; }, Props>, any>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'Matching<{ order: Readonly<PermittedOrderParams>; initialOrder: PermittedOrderParams; isConfirming: boolean; externalQuote: Readonly<ThirdPartyQuote>; panelQuote: Readonly<PanelQuote>; } & { ...; }, Props>' is not assignable to type 'Readonly<Props>'.
        Types of property 'confirmOrder' are incompatible.
          Type 'ThunkAction<void, State, void, Action<any>>' is not assignable to type '(usabilityTestId: number, priceInCents: number) => void'.

This was all working perfectly before, in fact 6.0.9 (which I'll be sticking to for now) was fantastic in that I was able to remove many of my type hacks. With your new version I'm forced to go back to my old tricks:

const mapDispatchToProps = {
  onCancel: cancelOrder,
  fetchExternalQuote,
  fetchPanelQuote,
  fetchQuoteQueued,
  confirmOrder,
  createOrder,
}

export const OrderForm = connect(
  (state: State) => ({
    order: getOrder(state),
    initialOrder: Order.createEmpty(),
    isConfirming: isConfirming(state),
    externalQuote: getExternalQuote(state),
    panelQuote: getPanelQuote(state),
  }),
  {
    onCancel: cancelOrder,
    fetchExternalQuote,
    fetchPanelQuote,
    fetchQuoteQueued,
    confirmOrder,
    createOrder,
  }
  // :'(
  mapDispatchToProps as Pick<ImplProps, keyof typeof mapDispatchToProps>
)(Impl)

Most helpful comment

I think we need TS 3.1 features to correctly type the result; it should be something like

type BoundThunkCreator<T extends (...args: any[]) => any> = T extends (
  ...args: infer A
) => (dispatch: (action: any) => any, getState: () => any, extra: any) => infer R
  ? (...args: A) => R
  : T

All 13 comments

Alternatively you could provide some guidance on how these types are intended to be used?

Alternatively you could provide some guidance on how these types are intended to be used?

You can take a look at the test I wrote.

In a nutshell the main condition is to have return type of action creator's prop in WrappedComponent to match the return type of actual action creator.

@rhys-vdw I suspect that in your codebase the problem is in ImplProps. Can you post it here along with the ConfirmOrder actual type signature?

I've just hit this in a very new project. This action creator:

export const clickTile = (x: number, y: number) => {
  return (dispatch: Dispatch, getState: () => State) => {
    const state = getState();
    if (selectTile(state, { x, y }).status !== "dug") {
      return dispatch(updateTile(x, y));
    }
  };
};

is added to this connect:

export const Tile = connect(
  (state: State, props: Pick<Props, "x" | "y">) => {
    return selectTile(state, { x: props.x, y: props.y });
  },
  { clickTile, startSelection },
)(TileBase);

When moused over:
dispatch inference
The typings are cutting out the outer function, not the thunk.

@threehams Correct, that's the usecase I haven't thought about.

I'll address it with the follow up PR. I'm going to expand type definition to check if the function returned by ThunkActionCreator also returns a function, and if it is - I'll cut the inner one.

@surgeboris

I suspect that in your codebase the problem is in ImplProps. Can you post it here along with the ConfirmOrder actual type signature?

Props here is aliased as ImplProps

interface Props {
  availableCountries: ReadonlyArray<string>
  isConfirming: boolean
  panelQuote: Readonly<PanelQuote>
  externalQuote: Readonly<ThirdPartyQuote>
  usabilityTestId: number
  confirmOrder: (usabilityTestId: number, priceInCents: number) => void
  createOrder: (
    usabilityTestId: number,
    targetPanel: Panel,
    order: Readonly<PermittedOrderParams>
  ) => void
  order: Readonly<PermittedOrderParams>
  fetchQuoteQueued: () => void
  fetchPanelQuote: (
    usabilityTestId: number,
    order: Readonly<PermittedOrderParams>
  ) => void
  fetchExternalQuote: (
    usabilityTestId: number,
    order: Readonly<PermittedOrderParams>
  ) => void
  initialOrder: Readonly<PermittedOrderParams>
  onCancel: () => void
}

And these are the action creator thunks:

export const fetchExternalQuote = (
  usabilityTestId: number,
  order: Readonly<PermittedOrderParams>
): AsyncThunkAction<State> => async dispatch => {
  // ...
}

export const fetchPanelQuote = (
  usabilityTestId: number,
  order: Readonly<PermittedOrderParams>
): AsyncThunkAction<State> => async dispatch => {
 // ...
}

export const confirmOrder = (
  usabilityTestId: number,
  princeInCents: number
): ThunkAction<State> => dispatch => {
 // ...
}

export const createOrder = (
  usabilityTestId: number,
  targetPanel: Panel,
  order: Readonly<PermittedOrderParams>
): AsyncThunkAction<State> => async dispatch => {
 // ...
}

And for completeness here is my AsyncThunkAction helper:

import { ThunkAction as ReduxThunkAction } from "redux-thunk"
import { Action } from "redux"

export type ThunkAction<State, Return = void> = ReduxThunkAction<
  Return,
  State,
  void,
  Action<any>
>
export type AsyncThunkAction<State, Return = void> = ThunkAction<
  State,
  Promise<Return>
>

@surgeboris

You can take a look at the test I wrote.

Your test is has a specific Promise return type. I wonder if this is why it passes? I haven't had a chance to look into it.

Your test is has a specific Promise return type. I wonder if this is why it passes?

@rhys-vdw I did not understood the issue fully. Now that you've posted complete information - your issue is about parametrized thunks, which is something I haven't thought about.

I'll try to address it in the follow up PR once I'll have some spare time for that (ETA end of the week to land the PR, and I'm not sure when exactly it will be merged).

This is happening even with non-parametrized thunks. The problem is that the unwrapping is expanding to be the thunk function itself (the thing that receives dispatch, getState, etc from the middleware) instead of being the action creator returning the thunk's result.

This does cause invoking the function to have the correct return type (hooray), but it has the wrong parameter types.

image

declare const mapped: WithThunkActionCreators<typeof dispatch>
const dispatch = {
  foo(): ThunkAction<true, void, void, AnyAction> {
    return dispatch => {
      dispatch({ type: 'foo' })
      return true
    }
  }
}
const result = mapped.foo()

This is happening even with non-parametrized thunks. The problem is that the unwrapping is expanding to be the thunk function itself (the thing that receives dispatch, getState, etc from the store) instead of being the action creator returning the thunk's result.

Thank you very much for clarification. It's definitely an oversight from my side.

This does cause invoking the function to have the correct return type (hooray), but it has the wrong parameter types.

It's because the initial concern in my codebase was about return types, and I focused too much on it, forgetting about proper types for the parameters. I'll address that as quickly as possible.

I think we need TS 3.1 features to correctly type the result; it should be something like

type BoundThunkCreator<T extends (...args: any[]) => any> = T extends (
  ...args: infer A
) => (dispatch: (action: any) => any, getState: () => any, extra: any) => infer R
  ? (...args: A) => R
  : T

Thanks @surgeboris :-)

Should work in @types/[email protected]

@rhys-vdw can you check and (if issue is resolved) close this issue?

This issue still exists, for async functions/thunks provided to mapDispatchToProps the outer function is omitted.

getting :

Type '() => Promise<Action<any>>' is not assignable to type '() => (
    dispatch: ThunkDispatch<FullState, undefined, AnyAction>
) => Promise<Action<any>>'
Was this page helpful?
0 / 5 - 0 ratings