1

I'm having trouble understanding how to properly type a function passed as a parameter to another function where the passed function can have 2 different signatures, one with a param the other without param.

I have a reduced case that looks like this:


type ApiMethod<T, U> = {
  (payload?: T): Promise<U>;
};

function callFactory<T, U>(apiMethod: ApiMethod<T, U>) {
  return async (payload?: T): Promise<U> => {
    if (payload) {
      return await apiMethod(payload);
    } else {
      return await apiMethod();
    }
  };
}

const apiMethodExample1: (payload: string) => Promise<string> = (payload) => {
  return Promise.resolve('some payload: ' + payload);
};

const apiMethodExample2: () => Promise<string> = () => {
  return Promise.resolve('no payload');
};

const call1 = callFactory(apiMethodExample1); // here TS complains
const call2 = callFactory(apiMethodExample2);

const value1 = call1('examplePayload').then((value: string) => console.log(value));
const value2 = call2().then((value) => console.log(value));

here the code in the TS playground

My problem is that TS complains that in

const call1 = callFactory(apiMethodExample1);

Argument of type '(payload: string) => Promise<string>' is not assignable to parameter of type 'ApiMethod<string, string>'.
  Types of parameters 'payload' and 'payload' are incompatible.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'.

I feel like I am not overloading the apiMethod param properly but all my other attempts have failed as well

I have looked at this answer: Typescript function overloads with generic optional parameters

but could not apply it to my case

thanks for any help

2
  • The problem here is that the ApiMethod definition includes functions with an optional payload. Since your apiMethodExample1 function requires it's payload argument, it is not assignable to ApiMethod where that argument is optional. Commented Jun 3, 2020 at 14:12
  • Thanks @CRice - I'm starting to understand. I'm just looking for a way to be able to pass both methods to the factory. Does that seem feasible? or, as I've started to think, the only way to cast methods to the proper type when passing them to the factory Commented Jun 4, 2020 at 6:42

2 Answers 2

4

Given this type:

type ApiMethod<T, U> = {
  (payload: T): Promise<U>;
  (payload?: T): Promise<U>;
};

If you give me a value f of type ApiMethod<string, string>, I should be able to call f("someString") and I should also be able to call f(). Overloaded functions have multiple call signatures and need to be callable for each of the call signatures.

If I call f() and everything explodes, then you haven't given me a valid ApiMethod<string, string>. And that's what the compiler is complaining about for apiMethodExample1.


Let me modify the implementation of apiMethodExample1 slightly:

const apiMethodExample1: (payload: string) => Promise<string> = (payload) => {
  return Promise.resolve('some payload: ' + payload.toUpperCase());
};

All I've done here is uppercase the payload, which is a string, so it should have a toUpperCase() method. This is no different from your version of apiMethodExample1 from the type system's point of view, since the implementation details are not visible from outside of the function.

If the compiler didn't complain about this:

const call1 = callFactory(apiMethodExample1); 

then because the type of call1 is inferred as

// const call1: (payload?: string | undefined) => Promise<string>

and so you are allowed to do this:

call1().then(s => console.log(s));

which explodes at runtime with

// TypeError: payload is undefined

The problem is that apiMethodExample1 can only be safely used as a (payload: string): Promise<string> and not as the full set of call signatures required by ApiMethod<string, string>.


Note that apiMethodExample2 is fine because the single signature () => Promise<string> is assignable to both call signatures. It might be surprising that () => Promise<string> is assignable to (payload: string) => Promise<string>, but that's because you can safely use the former as the latter, as the former will ignore any parameter passed to it. See the TypeScript FAQ entry called Why are functions with fewer parameters assignable to functions that take more parameters? for more information.


Aside: note that if your code weren't just a reduced example, I'd strongly suggest removing the first overload signature because any function that satisfies the second one will also satisfy the first one. So this particular example doesn't really have to do with overloads per se; you get the same behavior if you just write

type ApiMethod<T, U> = {
  (payload?: T): Promise<U>;
};

Okay, hope that helps; good luck!

Playground link to code


UPDATE:

It looks like you want to type callFactory() to accept both types, and not that you really care about ApiMethod<T, U> at all. If so, I'd write it this way:

function callFactory<T extends [] | [any], U>(apiMethod: (...payload: T) => Promise<U>) {
  return async (...payload: T): Promise<U> => {
    return await apiMethod(...payload);
  };
}

No conditional code inside the implementation; it just spreads the arguments into the call. And callFactory is typed so that the function it returns takes the same arguments as the passed-in apiMethod. It's not clear why you even need callFactory since all it does is return something of the same type as its argument (callFactory(apiMethodExample1) and apiMethodExample1 are basically the same thing), but that's your reduced example code, I guess.

Anyway, everything after that will just work:

const call1 = callFactory(apiMethodExample1); // okay
const call2 = callFactory(apiMethodExample2); // okay

const value1 = call1('examplePayload').then((value: string) => console.log(value));
const value2 = call2().then((value) => console.log(value));

call1() // error
call2("hey"); // error

Hope that helps. Good luck again.

Playground link to code

Sign up to request clarification or add additional context in comments.

7 Comments

This is a very clear, well thought out explanation. Thank you.
Thanks a lot for the explanation, I'm still struggling to really understand so I have to spend a little time with it. For now I'll edit the question to remove the unnecessary overload
so, I have found that the only way to make this work would be to assert that apiMethodExample1 has to be used as ApiMethod<string, string>. Is that right? Or is there a way I can modify the callFactory to accept both methods without having to cast ?
I updated my answer... assuming you want to "make things work" by ignoring the ApiMethod definition you gave and just type callFactory() with enough flexibility to support the way you want to call it
Thanks again, this looks like what I'm after indeed. Being able to pass a method that accepts 0 or more arguments as my apiMethods can have. The factory is useless here I know but it is used to add functionality (logging ... ) and the code for that has been omitted (maybe there is a better way ...) - as for the way you wrote it this time I can't say I understand it right now, I'm struggling with the extends and spread part, any resource you'd recommend?
|
1

I've added "overloads" to methods in classes before with this pattern.

genUpdateItem<T>(data: [T, IStringAnyMap]): void;

genUpdateItem<T, S>(data: [T, IStringAnyMap], options?: S): void;

genUpdateItem<T, S>(data: [T, IStringAnyMap], options?: S): void {
  // do the work in this method
}

1 Comment

Thanks, I was hoping I could find find a way to do just that for the method I'm passing to the factory, I might (...) be confused but I don't see how I can do it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.