Home > Software design >  Mapped function type
Mapped function type

Time:01-29

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.

Playground link to code

CodePudding user response:

try this:

type Result = { [K in keyof TypeMap]: (key: K) => TypeMap[K] }[keyof TypeMap];
  • Related