首页 > 解决方案 > 我如何告诉打字稿元组中的最后一个参数始终是一个函数?

问题描述

我制作了一个函数,它使用扩展运算符来接受具有不同数量参数的重载。不幸的是,打字稿说函数的参数之一是不可调用的。下面是我所指的一个例子。

type Arguments=[{():void}]|[string,{():void}]|[string,number,{():void}];

function overload(...args:Arguments){
  //Do stuff depending on number of arguments
  if(args.length===2)console.log(args[0]);
  if(args.length===3)console.log(args[0],args[1]);

  args[args.length-1](); //TS error here
}
//An example of how I'd like to call it
overload(()=>console.log(`I'm always the last..`));
overload('example',()=>console.log(`..and I'm a function..`));
overload('example',1,()=>console.log(`..that can be called`));

最后一个参数始终是一个函数,因为元组[{():void}]|[string,{():void}]|[string,number,{():void}]每个都以{():void}. 因此它可以这样调用args[args.length-1]();,但我得到:

此表达式不可调用。并非所有类型为 'string | 号码 | (() => void)' 是可调用的。类型“字符串”没有调用签名.ts(2349)

我的问题是:无论如何,我如何告诉 typescript - 上面显示的数组的最后一项是我可以调用的函数?这是做这样的事情的最佳方式吗?关于如何以更好的方法获得与上述相同结果的任何建议?

我试过了:

//NOPE
function overload(...args:Arguments):void{
  /* ... */
  if(typeof args[args.length-1]==='function'){
    args[args.length-1]();
  }
}

//NOPE
function overload(...args:[{():void}]):void;
function overload(...args:[string,{():void}]):void;
function overload(...args:[string,number,{():void}]):void;
function overload(...args:[{():void}]|[string,{():void}]|[string,number,{():void}]):void{
  /* ... */
  args[args.length-1]();
}

我知道我可以做这样的事情来克服这个问题:

function overload(...args:Arguments){
  /* ... */
  const last=args[args.length-1] as {():void};
  last();
}

但我只调用了数组的最后一项。我不想仅仅为了让它工作而制作一个特殊的变量,这也是我编写类型的原因——为了摆脱错误而不是创建新的“假”,对吧?这是args.length-1TS 无法处理的某种未知值吗,但如果是这样,为什么 TS 假定它是“字符串 | 号码 | (() => void)' 类型?

代码按预期工作(用纯 javascript 编写)。原始代码比这个高级一点,因此,我对其进行了简化,在我看来它并没有改变它的主要含义。如果重要的话,我正在使用 typescript 3.9.5。

标签: typescript

解决方案


您希望编译器识别args[args.length-1]将返回元组的最后一个元素,但目前不支持。

编译器知道args[0]返回第一个元素,并且索引到具有数字文字类型的元组也将起作用,但即使编译器将args.lengthand1视为数字文字类型,减法运算只会产生number. 您目前无法使用数字文字类型进行数学运算;请参阅microsoft/TypeScript#26382以获取更改它的公开建议。即使这样,您也可能会遇到联合类型之类的问题Arguments,因为编译器需要意识到 of 的类型与 ofargs.length-1的类型相关args,而编译器也不擅长这一点。有关详细信息,请参阅microsoft/TypeScript#30581


更有希望的是有一个last()函数的想法,它接受一个类型的元组T并返回一个类型的值,Last<T>其中Last提取元组的最后一个元素的类型。这并不容易,因为 TypeScript 的元组类型操作支持目前围绕在 rest parameters 中使用它们,所以你需要跳过涉及函数类型的箍。有一些开放的问题要求进行更一般的操作,例如microsoft/TypeScript#26223,但还没有任何东西是该语言的一部分。

此外,最好避免循环条件类型(请参阅microsoft/TypeScript#26980),因此以下操作不会使用它们。

首先我会写Tail<T>,它接受一个元组T并返回一个新元组,它T与删除它的第一个元素相同。如果T[1,2,3],那么Tail<T>[2,3]

type Tail<T extends readonly any[]> =
    ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;

而现在Last<T>可以用 来定义Tail<T>。方法是:如果T是数组或开放式元组,则只需T使用一些非常大的数字索引进行索引。否则,找到其中一个键T不是 的键Tail<T>,并以此为索引T。例如,您可以验证Last<[1, 2, 3]>is 3

type Last<T extends readonly any[]> = number extends T['length'] ? T[1e100] : {
    [K in keyof T]: K extends keyof Tail<T> ? never : T[K] }[number];

然后我们会last()这样声明:

function last<T extends readonly any[]>(t: T): Last<T>;
function last(t: any[]) {
    return t[t.length - 1];
}

请注意,编译器无法验证它t[t.length-1]是 type Last<T>,因此我们需要断言(这里我使用的是单个重载,其实现签名比调用签名更宽松)。


有了last(),我们可以试试你的overload()

function overload(...args: Arguments) {
    if (args.length === 2) console.log(args[0]);
    if (args.length === 3) console.log(args[0], args[1]);
    last(args)(); // okay
}

这样可行!如果我检查 的类型last(args),你会看到你所期望的:

const f = last(args);
// const f: () => void

如果您更改Arguments联合中任何元组的最后一个元素,使其不是零参数函数类型,编译器会警告您:

function badOverload(...args: Arguments | [string, number, boolean, string]) {
    const f = last(args);
    // const f: string | () => void
    last(args)(); // error! string is not callable
}

好的,希望对你有用。祝你好运!

Playground 代码链接


推荐阅读