Home > Mobile >  Infer a parameter of an overload function
Infer a parameter of an overload function

Time:10-27

Is it possible to infer a parameter of an overload function?

For instance:

type MyFunction = {
  (key: "one", params: { first: string }): void;
  (key: "two", params: { second: string }): void;
}

type MyFunctionParams<T extends string> = MyFunction extends (
  key: T,
  params: infer P,
) => void
  ? P
  : never;

// should be `{ first: string }`
type OneParams = MyFunctionParams<"one"> // never

Typescript playground

CodePudding user response:

If you control the definition of MyFunction, I would follow caTS's excellent approach of coming at it from another angle. (It's striking sometimes how if you change your starting point, you can simplify something or even make something possible that wasn't originally possible, often without negative consequences.)

If you have to use an existing MyFunction type, though, then: Two possible solutions for you, a general one and a more pragmatic targeted one, both frankly not as satisfying as we would all probably like:

The General Solution

It is, sadly, very difficult and verbose — but possible — to access the parameter information (or return type information) of function overload types. As described in this question's answers, it's just a limitation of Parameters and ReturnType that they don't handle overloads the way we might want. Your question is a great example of that. Here's a simpler one (playground):

type MyFunction = {
    (key: "one", params: { first: string }): void;
    (key: "two", params: { second: string }): void;
};

type MyFunctionKey = Parameters<MyFunction>[0];
//   ^? type MyFunctionKey = "two"
// Huh?!? I think most of us would expect `"one" | "two"`, but
// that's just not the way it is. (There's usually a good reason
// for these limitations in the general case, but...)

But it is possible within reason, where "reason" in this case is a sensible maximum number of possible overloads of the function. Let's take six as our maximum overloads limit.

As you can see from Titian's answer to that question, it's possible to know how many overloads a function has, and to infer their parameter lists. For example:

type Whatever<F> =
    F extends {
        (...args: infer Params1): any);
        (...args: infer Params2): any);
    }
        ? // This branch is used if F has **exactly** two overloads
        : // Otherwise this one is
;

We can use that mechanism to handle 2, 3, 4, 5...up to whatever reasonable limit you want possible sets of overloads. So here's the first part of our puzzle, the MyFunctonParams that dispatches based on how many overloads there are to a parameters helper type:

type MyFunctionParams<KeyType> =
    MyFunction extends {
        (...args: infer P1): any;
        (...args: infer P2): any;
    }
        ? ParamsHelper<KeyType, P1, P2>
        : MyFunction extends {
              (...args: infer P1): any;
              (...args: infer P2): any;
              (...args: infer P3): any;
          }
        ? ParamsHelper<KeyType, P1, P2, P3>
        : MyFunction extends {
              (...args: infer P1): any;
              (...args: infer P2): any;
              (...args: infer P3): any;
              (...args: infer P4): any;
          }
        ? ParamsHelper<KeyType, P1, P2, P3, P4>
        : MyFunction extends {
              (...args: infer P1): any;
              (...args: infer P2): any;
              (...args: infer P3): any;
              (...args: infer P4): any;
              (...args: infer P5): any;
          }
        ? ParamsHelper<KeyType, P1, P2, P3, P4, P5>
        : MyFunction extends {
              (...args: infer P1): any;
              (...args: infer P2): any;
              (...args: infer P3): any;
              (...args: infer P4): any;
              (...args: infer P5): any;
              (...args: infer P6): any;
          }
        ? ParamsHelper<KeyType, P1, P2, P3, P4, P5, P6>
        : never;

ParamsHelper's job is to match the KeyType we provide with the first element of each parameter tuples we pass it (your key) and provide the type of the second element (your params):

type ParamsHelper<
    KeyType,
    P1 extends any[],
    P2 extends any[] = [never, never],
    P3 extends any[] = [never, never],
    P4 extends any[] = [never, never],
    P5 extends any[] = [never, never],
    P6 extends any[] = [never, never]
> = KeyType extends P1[0]
    ? P1[1]
    : KeyType extends P2[0]
    ? P2[1]
    : KeyType extends P3[0]
    ? P3[1]
    : KeyType extends P4[0]
    ? P4[1]
    : KeyType extends P5[0]
    ? P5[1]
    : KeyType extends P6[0]
    ? P6[1]
    : never;

That's it! Now we get the desired result, provided MyFunction doesn't have more than six overloads (or whatever limit you decide to set):

type OneParams = MyFunctionParams<"one">;
//   ^? type OneParams = { first: string; }

type TwoParams = MyFunctionParams<"two">;
//   ^? type TwoParams = { second: string; }

Playground example

In that example, MyFunctionParams is tied to MyFunction, but we could make a generic version if we wanted that we passed the function type into. Seemed overkill for your example, but here that is.

The Pragmatic Targeted Solution

If we just want to handle MyFunction, we might take a more concise and targeted approach, like this:

type MyFunction1 = {
    (key: "one", params: { first: string }): void;
};
type MyFunction2 = {
    (key: "two", params: { second: string }): void;
};
type MyFunction = MyFunction1 & MyFunction2;

type MyFunctionParams<KeyType> =
    KeyType extends Parameters<MyFunction1>[0]
    ? Parameters<MyFunction1>[1]
        : KeyType extends Parameters<MyFunction2>[0]
        ? Parameters<MyFunction2>[1]
            : never;

type OneParams = MyFunctionParams<"one">;
//   ^? type OneParams = { first: string; }

type TwoParams = MyFunctionParams<"two">;
//   ^? type TwoParams = { second: string; }

Playground link

Not nearly as satisfying, and of course every time you add another overload, you have to edit the MyFunctionParams type to handle it, but sometimes something simpler and targeted is better, it depends on your situation.

CodePudding user response:

Let's do the "opposite" here... Using a generic type, and a map of keys to parameters, we can get the original MyFunction type back like this:

type PMap = {
    one: { first: string };
    two: { second: string };
};

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type MyF<T extends keyof PMap> = (key: T, params: PMap[T]) => void;

type MyFunction = UnionToIntersection<keyof PMap extends infer U extends keyof PMap ? U extends U ? MyF<U> : never : never>;

And this means you'll be able to get what you want like this:

type MyFunctionParams<T extends keyof PMap> = Parameters<MyF<T>>[1];

Quite long but extensible and does the job.

Playground

  • Related