typescript 函数定义的下一个参数取决于第一个参数

34gzjxbg  于 2022-11-18  发布在  TypeScript
关注(0)|答案(2)|浏览(143)

请考虑一组具有不同参数的简单函数:

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

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

};

它们都返回相同的类型(bool);
然后是另一个函数,该函数应该调用上述任何函数:

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

怎样才能使args反映出所选fn的参数呢?
例如:

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

并让参数显示在vscode提示中,就像一般函式一样。
我知道我需要为每个fn创建validateField重载,但如何使用类型定义或其他方法来实现这一点...而不必手动定义每个重载并使用这些参数编写重复的代码

gmxoilav

gmxoilav1#

您可能希望validateField()fn参数的类型中为generic,这样您就可以为args选择合适的类型。我们可以编写一些辅助实用程序类型来计算它:

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[]];
} */

FnArgs类型是mapped类型,其中每个键都来自fns类型,并且每个值都是初始string之后的参数元组(使用条件类型推断来获得该列表)。
现在,您可以为validateField()指定以下调用签名:

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

当您调用它时,它将工作:

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

不幸的是,validateField()的实现没有类型检查:

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.
}

根本问题是缺少对microsoft/TypeScript#30581中所请求的 *correlated union * 的直接支持。编译器无法理解fns[fn]的类型是与args的类型相关的函数类型。错误消息有点晦涩,但它来自于这样的事实,即它将args视为不适合于fns[fn]的自变量的元组类型的并集,它将fns[fn]视为没有公共rest参数类型的函数类型的并集。
幸运的是,在microsoft/TypeScript#47109中有一个推荐的解决方案。我们需要给予fns一个新的类型,编译器一眼就能看到这个类型是一个对象,它的方法的参数与FnArgs直接相关。下面是它的外观:

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
}

_fns变量被注解为方法的Map类型,对于Fns的键中的每个K都显式使用类型为FnArgs[K]的rest参数。fns对该变量的赋值成功,因为它是相同的类型。
但关键的区别在于_fns[fn](where, ...args)成功,而fns[fn](where, ...args)失败,这是因为编译器跟踪了_fns[fn]类型和args类型之间在泛型K中的相关性。
现在,您就拥有了对调用者和函数实现都能按预期工作的东西!
Playground代码链接

s4n0splo

s4n0splo2#

要解决您的问题,可以使用泛型类型获取函数类型,然后使用此类型获取参数的类型

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

相关问题