Thomas Mery
Thomas Mery

Reputation: 120

Typescript function overload, generic optional parameter

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

Upvotes: 1

Views: 2624

Answers (2)

jcalz
jcalz

Reputation: 328758

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

Upvotes: 4

joshvito
joshvito

Reputation: 1563

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
}

Upvotes: 1

Related Questions