14

I have a redux thunk action that fetches some data and then dispatches some actions (not shown in the code here, but you'll be able to find it in the demo link bellow)

export const fetchPosts = (id: string) => (dispatch: Dispatch<TActions>) => {
    return fetch('http://example.com').then(
    response => {
        return response.json().then(json => {
        return "Success message";
        });
    },
    err => {
        throw err;
    }
    );
};

and than in my component I use mapDispatchToProps with bindActionCreators to call this function from my component like so:

public fetchFunc() {
    this.props.fetchPosts("test").then(
        res => {
        console.log("Res from app", res);
        },
        err => {
        console.log("Err from app", err);
        }
    );
}

Since I am using typescript, I need to define the type of this function in the Props

interface IProps {
    name?: string;
    posts: IPost[];
    loading: boolean;
    fetchPosts: (id: string) => Promise<string | Error>;
}

If I do-it like above, Typescript will complain that I should do-it like this:

fetchPosts: (id: string) => (dispatch: Dispatch<TActions>) => Promise<string | Error>; 

If I do-it like this, then Typescript complains when I use then in my component saying that that function is not a promise.

I created a demo where you can fiddle with the code

Pressing "Load from remote" will sometimes fail just to see if the promise:

https://codesandbox.io/s/v818xwl670

Adrian Florescu
  • 4,454
  • 8
  • 50
  • 74
  • I saw this question when you first posted and banged my head for a while but could not figure it out. I belive the signature of `bindActionCreators` is wrong, since at runtime if you pass it a function that returns a function, the result is a function that will have the second function invoked automatically with `dispatch`, but the typings don't reflect this, they just return a function with the same type. But I am not knowledgeable enough with redux to state this for sure – Titian Cernicova-Dragomir Mar 27 '18 at 19:08
  • To get around this I used a type assertion, and it both compiles and has the expected runtime behavior but it feels hackish: `bindActionCreators( { fetchPosts: fetchPosts as any as ((id: string) => Promise) }, dispatch );` – Titian Cernicova-Dragomir Mar 27 '18 at 19:11

4 Answers4

8

The problem is the call to bindActionCreators in mapDispatchToProps. At runtime bindActionCreators basically transforms this (id: string) => (dispatch: Dispatch<TActions>) => Promise<string>; into this (id: string) => Promise<string>;, but the type for bindActionCreators does not reflect this transformation. This is probably due to the fact that to accomplish this you would need conditional types which until recently were not available.

If we look at this sample usage from the redux repo, we see that they accomplish the transformation by specifying the types of the functions explicitly:

const boundAddTodoViaThunk = bindActionCreators<
  ActionCreator<AddTodoThunk>,
  ActionCreator<AddTodoAction>
>(addTodoViaThunk, dispatch)

We could do the same in your code, referencing existing types, but this hurts type safety as there is no check that fetchPosts in the two types will be correctly typed:

const mapDispatchToProps = (dispatch: Dispatch<TActions>): Partial<IProps> =>
  bindActionCreators<{ fetchPosts: typeof fetchPosts }, Pick<IProps, 'fetchPosts'>>(
    {
      fetchPosts
    },
    dispatch
  );

Or we could use a type assertion since the above method does not really offer any safety anyway:

const mapDispatchToProps2 = (dispatch: Dispatch<TActions>) =>
    bindActionCreators({ 
      fetchPosts: fetchPosts as any as ((id: string) => Promise<string>) 
    }, dispatch ); 

For a truly type safe way to do this we need to use typescript 2.8 and conditional types with a helper function. We can type bindActionCreators the way it should, and automatically infer the correct type for the resulting creators:

function mybindActionCreators<M extends ActionCreatorsMapObject>(map: M, dispatch: Dispatch<TActions>) {
  return bindActionCreators<M, { [P in keyof M] : RemoveDispatch<M[P]> }>(map, dispatch);
}
const mapDispatchToProps = (dispatch: Dispatch<TActions>) =>
  mybindActionCreators(
    {
      fetchPosts
    },
    dispatch
  );

// Helpers
type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;

type RemoveDispatch<T extends Function> =
  T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F, g: infer G, h: infer H, i: infer I, j: infer J) => (dispatch: Dispatch<any>) => infer R ? (
    IsValidArg<J> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => R :
    IsValidArg<I> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => R :
    IsValidArg<H> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => R :
    IsValidArg<G> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => R :
    IsValidArg<F> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F) => R :
    IsValidArg<E> extends true ? (a: A, b: B, c: C, d: D, e: E) => R :
    IsValidArg<D> extends true ? (a: A, b: B, c: C, d: D) => R :
    IsValidArg<C> extends true ? (a: A, b: B, c: C) => R :
    IsValidArg<B> extends true ? (a: A, b: B) => R :
    IsValidArg<A> extends true ? (a: A) => R :
    () => R
  ) : T;
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
2

Basically, in Typescript, the generic type of the promise will be inferred from the resolve only.

For example

function asyncFunction() {
    return new Promise((resolve, reject) => {
       const a = new Foo();
       resolve(a);
    })
}

asynFunction return type will be inferred as Promise<Foo>

You just only need to remove Error as a union type in your type to get the proper type definition:

fetchPosts: (id: string) => (dispatch: Dispatch<TActions>) => Promise<string>;

Thai Duong Tran
  • 2,453
  • 12
  • 15
  • Thanks Thai, I tried adding the type you mentioned, but it sayid that `then` does not exist on type dispatch. Feel free to fiddle with the codesandbox link posted above – Adrian Florescu Mar 22 '18 at 09:48
1

Thank you @Thai Duong Tran and @Titian Cernicova-Dragomir.

I've found a mix between the two answers you provided.

1:

In the props, instead of redeclaring all argument types and return types, I can say that the function has the typeof the original function as so: fetchPosts: typeof fetchPosts (Thanks to @titian-cernicova-dragomir)

2:

Now I can use that function, but not as a promise. to make that work as a promise, I can use the solution provided by @thai-duong-tran.

const fetchPromise = new Promise(resolve => {
    this.props.fetchPosts("adidas");
});

You can see the working demo here: https://codesandbox.io/s/zo15pj633

Adrian Florescu
  • 4,454
  • 8
  • 50
  • 74
  • First of all, nice work. I just testet your code in the latest CRA and it gives an error in fetchPosts dispatches; (TS) Argument of type '{ type: string; loading: boolean; }' is not assignable to parameter of type 'TActions'. Type '{ type: string; loading: boolean; }' is not assignable to type 'IALoadingPosts'. Types of property 'type' are incompatible. Type 'string' is not assignable to type '"LOADING_POSTS"'. Any idea how this can be fixed? – keysersoze Jun 20 '18 at 17:06
0

Try the following:

fetchPosts: (id: string) => void;