Home > OS >  Next arguments of function definition depending on first argument
Next arguments of function definition depending on first argument

Time:11-17

Consider a list of simple functions with different arguments:

const fns = {
  isValidDate: (input: string, min?: Date, max?: Date): boolean => {
     // ...
     return true;
  },

  isValidOption: (input: string, options: string[]): boolean => {
     // ...
     return true;
  },

};

They all return the same type (bool);

Then another function that is supposed to call any of the functions above:

function validateField(where: string, fn: keyof typeof fns, ...args: any[]){
   // ...
   return fns[fn](...args);
}

How can I make args reflect the parameters of the chose fn ?

For example:

validateField("test", "isValidDate", new Date()); // should be ok 
validateField("test", "isValidDate", 123); // should fail

and have the arguments show in vscode hints, like on normal functions.

I know I need to create overloads for validateField for each fn, but how to do that with a type definitions or something... without having to manually define each overload and write duplicate code with those arguments

CodePudding user response:

You probably want validateField() to be generic in the type of the fn parameter so you can choose the appropriate type for args. We can write some helper utility types to compute this:

type Fns = typeof fns;

type FnArgs = { [K in keyof Fns]: 
  Fns[K] extends (input: string, ...args: infer A) => boolean ? A : never 
};

/* type FnArgs = {
    isValidDate: [min?: Date | undefined, max?: Date | undefined];
    isValidOption: [options: string[]];
} */

The FnArgs type is a mapped type where each key comes from the type of fns, and each value is the tuple of parameters after the initial string (using conditional type inference to get that list).

Now you can give validateField() this call signature:

declare function validateField<K extends keyof Fns>(
  where: string, fn: K, ...args: FnArgs[K]
): boolean;

and it will work when you call it:

validateField("test", "isValidDate", new Date()); //  okay
validateField("test", "isValidDate", 123); // error! number is not assignable to date
validateField("test", "isValidOption", ["a"]) // okay

Unfortunately the implementation of validateField() does not type check:

function validateField<K extends keyof Fns>(where: string, fn: K, ...args: FnArgs[K]) {
  return fns[fn](where, ...args); // error!  
  // -----------------> ~~~~~~~
  // A spread argument must either have a tuple type or 
  // be passed to a rest parameter.
}

The underlying issue is lack of direct support for correlated unions as requested in microsoft/TypeScript#30581. The compiler is not able to understand that the type of fns[fn] is of a function type that is correlated with the type of args. The error message is a bit cryptic, but it comes from the fact that it sees args as a union of tuple types inappropriate for the arguments of fns[fn], which it sees as a union of function types without a common rest parameter type.

Luckily there's a recommended solution for this described in microsoft/TypeScript#47109. We need to give fns a new type that the compiler can see at a glance is an object with methods whose parameters are directly related to FnArgs. Here's how it looks:

function validateField<K extends keyof Fns>(where: string, fn: K, ...args: FnArgs[K]) {
  const _fns: { [K in keyof Fns]: (str: string, ...args: FnArgs[K]) => boolean } = fns;
  return _fns[fn](where, ...args); // okay
}

The _fns variable is annotated as being of a mapped type of methods explicitly with a rest parameter of tpe FnArgs[K] for every K in the keys of Fns. The assignment of fns to that variable succeeds, because it's the same type.

But the crucial difference is that _fns[fn](where, ...args) succeeds where fns[fn](where, ...args) fails. And that's because the compiler has kept track of the correlation across the generic K between the type of _fns[fn] and the type of args.

And now you have something that works as desired for both the callers and the implementation of the function!

Playground link to code

CodePudding user response:

To solve your question, you can use the Generic type to get the function type and then use this to get the type of the parameter

function validateField<key extends keyof typeof fns>(where: string, fn: key, ...options: Parameters<typeof fns[key]>): boolean {
  const fnToCall = fns[fn];
  return fnToCall(...options);
}
  • Related