TypeScript considers a function with multiple call signatures (also called an overloaded function) to be equivalent to an intersection of these call signatures. That is:
type Result = {
(key: "a"): A;
(key: "b"): B;
};
behaves the same as
type Result = { (key: "a"): A; } & { (key: "b"): B; };
which is the same as
type Result = ((key: "a") => A) & ((key: "b") => B);
As you can verify by testing the below code with any of those Result
definitions:
declare const r: Result;
const a = r("a");
// const a: A
const b = r("b");
// const b: B
So if we can generate an intersection programmatically, it will be equivalent to what you're asking for.
Luckily we can do this, via a conditional type inference technique involving type parameters in contravariant positions (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript ). It's similar to the answer to Transform union type to intersection type :
type Result = {
[K in keyof TypeMap]: (x: (key: K) => TypeMap[K]) => void
}[keyof TypeMap] extends (x: infer I) => void ? I : never;
// type Result = ((key: "a") => A) & ((key: "b") => B)
except that it's a distributive object type as coined in ms/TS#47109 where we map over the keys of TypeMap
and immediately index into the mapped type with keyof typeMap
to get a union (which becomes an intersection due to the aforementioned contravariance). It uses supported and standard, if advanced, features of TypeScript.
But you might not really even need multiple call signatures, depending on the use case. Since each call signature maps the input to the output in the same way, by indexing into TypeMap
with one of its keys, you could get a very similar function type with a single generic call signature:
type Result = <K extends keyof TypeMap>(key: K) => TypeMap[K];
That's a lot simpler to write than the distributive contravariant object thingy above, and it behaves very similarly:
declare const r: Result;
const a = r("a");
// const a: A
const b = r("b");
// const b: B
Maybe your use case actually needs multiple distinct call signatures, but if not, I'd definitely recommend using generics here instead.
Playground link to code