What you'd like to do is make bar() generic in the type of the type parameter, like so:
function bar<K extends FooMedia>(type: K) {
return getFoo("t", type); // error!
}
But unfortunately TypeScript doesn't really know how to perform the higher order analysis on overloaded functions in order to figure out how to deal with getFoo("t", type) for generic type.
Overloads are good for the situation when you explicitly call it with code statically known to match one of the call signatures. Anything else is pretty much beyond the compiler's abilities.
You can call getFoo("", "article") is fine because the compiler can resolve that to the getFoo(id: string, type: "article"): Promise<Article>; call signature.
But getFoo("", Math.random()<0.5 ? "article" : "course") is not fine. You get an error that no overload matches the call. The compiler doesn't propagate the union type of the second parameter through to a union of call signatures (this is requested at microsoft/TypeScript#14107). And the same problem happens with getFoo("", type) for generic type. Since type isn't known to be exactly one specific acceptable value, the compiler doesn't know which call signature to use, and it gives up.
You can "fix" it and avoid the error in bar() by adding a call signature that works for the union type of the type:
// add this signature
function getFoo(
id: string, type: FooMedia
): Promise<AxiosResponse<Course | Article | Podcast, any>>;
but TypeScript doesn't synthesize or infer generic call signatures from specific ones so you'd get a union out:
function bar<K extends FooMedia>(type: K) {
return getFoo("t", type); // no error, but
}
const z = bar("course");
// const z: Promise<AxiosResponse<Course | Article | Podcast, any>>
Overloads are unfortunately not the tool for the job.
If you want bar() to be generic and have it call getFoo(), then getFoo() should also be generic. You can almost always turn an overloaded function call signature set into a single generic one, but in the general case it's kind of annoying. Say you had this:
declare function ovld(x: string, y: number): boolean;
declare function ovld(x: number): string;
declare function ovld(x: boolean, y: string): number;
ovld(3).toUpperCase();
ovld(true, "abc").toFixed(2);
You could refactor to an input-output mapping type like
type CallSignatures =
{ i: [x: string, y: number], o: boolean } |
{ i: [x: number], o: string } |
{ i: [x: boolean, y: string], o: number };
And write a conditional type to extract the output for a given input:
type Out<C extends { i: any[], o: any }, I extends any[]> =
C extends unknown ? I extends C['i'] ? C['o'] : never : never;
And test it out:
declare function gnrc<I extends CallSignatures['i']>(...args: I): Out<CallSignatures, I>;
gnrc(3).toUpperCase();
gnrc(true, "abc").toFixed(2);
That works, but it's kind of yucky. However in this case we can simplify considerably. All the call signatures look the same except for a single string literal type input parameter. And the output types are also the same except for a type that's a function of that string literal input type. And TypeScript already has great support for a mapping from string literal inputs to arbitrary outputs... it's called an interface:
interface FooMap {
course: Course,
article: Article,
podcast: Podcast
}
type FooMedia = keyof FooMap;
declare function getFoo<K extends FooMedia>(
id: string,
type: K
): Promise<AxiosResponse<FooMap[K], any>>;
That's very straightforward. Now, if you want, you can still implement this as an overload, albeit one with a single call signature:
// call signature
function getFoo<K extends FooMedia>(
id: string,
type: K
): Promise<AxiosResponse<FooMap[K], any>>;
// implementation
function getFoo(id: string, type: FooMedia) {
return null!;
}
People do that sort of thing when the compiler can't verify that the implementation matches the call signature, since overload implementations are checked more loosely than regular function implementations. Okay, let's see if bar() works now:
function bar<K extends keyof FooMap>(type: K) {
return getFoo("t", type);
}
// function bar<K extends keyof FooMap>(
// type: K): Promise<AxiosResponse<FooMap[K], any>>
The inferred return type is nice and generic now, which means calling bar() should result in the types you want:
const z = bar("course");
// const z: Promise<AxiosResponse<Course, any>>
Hooray!
Playground link to code
keyofand the map the reason this works or could it be done another way?