Reputation: 120
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
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!
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.
Upvotes: 4
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