Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Mapped function type

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

>Solution :

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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading