I am trying to construct a function type based on a utility type that is pre-existing, and simply defines a key-to-type mapping:
type TypeMap = {
a: A;
b: B;
}
The type I am trying to build is a multi-signature function type, using the key as a literal string in the first parameter:
type Result = {
(key: "a"): A;
(key: "b"): B;
}
Is this something that is possible in TypeScript? I know function types don't always place nicely with mapped types.
I could do something like this, but I would like to avoid repeating the full list of keys:
type TempFunc<K extends keyof TypeMap> = {
(key: K): TypeMap[K];
};
type Result = TempFunc<"a"> & TempFunc<"b">;
Note: this is a very over-simplified version of what I'm trying to accomplish; my actual TypeMap
has over 100 keys.
CodePudding user response:
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.
CodePudding user response:
try this:
type Result = { [K in keyof TypeMap]: (key: K) => TypeMap[K] }[keyof TypeMap];