【问题标题】:How can I tell typescript that the last argument in tuple is always a function?我如何告诉打字稿元组中的最后一个参数始终是一个函数?
【发布时间】:2020-09-26 16:05:53
【问题描述】:

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

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-1 TS 无法处理的某种未知值吗,但如果是这样,为什么 TS 假定它是“字符串 |号码 | (() => void)' 类型?

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

【问题讨论】:

    标签: typescript


    【解决方案1】:

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

    编译器理解 args[0] 返回 first 元素,并且使用 numeric literal type 索引到元组中也可以,但即使编译器将 args.length1 视为作为数字文字类型,减法运算仅产生number。您目前无法使用数字文字类型进行数学运算;请参阅microsoft/TypeScript#26382 以获取更改此设置的公开建议。即使这样,你可能会遇到像Arguments 这样的联合类型的问题,因为编译器需要意识到args.length-1 的类型与args 的类型相关,编译器也不擅长。请参阅microsoft/TypeScript#30581 了解更多信息。


    更有希望的是有一个last() 函数的想法,它接受T 类型的元组并返回Last<T> 类型的值,其中Last 提取元组的最后一个元素的类型。这并不容易,因为 TypeScript 的元组类型操作支持目前以 using them in 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&lt;T&gt; 可以用Tail&lt;T&gt; 来定义。方法是:如果T 是一个数组或open-ended tuple,那么只需使用一些非常大的数字索引来索引T。否则,找到T 的一个键,它不是Tail&lt;T&gt; 的键,并以此索引到T。例如,您可以验证Last&lt;[1, 2, 3]&gt;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] 是否属于Last&lt;T&gt; 类型,因此我们需要断言(这里我使用单个overload,其实现签名比调用签名更宽松)。


    有了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 link to code

    【讨论】:

      【解决方案2】:

      您可以在调用之前将数组项显式转换为() =&gt; void。单独的类型检查不足以提醒 typescript 数组项是可调用的。

      if(typeof args[args.length-1]==='function'){
          (args[args.length - 1] as () => void)();
      }
      

      【讨论】:

        【解决方案3】:

        似乎你想实现这样的目标:

        type Args<T> = [...T[], () => void];
        
        function process<T>(...args: Args<T>) {
            let [...values, func] = args;
            for (let value of values) {
                console.log(value);
            }
            func();
        }
        

        很遗憾,这是不可能的,因为 rest 运算符只能应用于元组类型的最后一个元素,并且必须是 javascript 数组解析中的最后一个表达式。

        可能的解决方法是将函数类型作为元组中的第一个元素:

        type Args<T> = [() => void, ...T[]];
        
        function process<T>(...args: Args<T>) {
            let [func, ...values] = args;
            for (let value of values) {
                console.log(value);
            }
            func();
        }
        
        process(() => console.log("process"));
        process(() => console.log("process"), 1, 2, 3);
        

        【讨论】:

        • 感谢您的努力,但这并不是我的意思。我希望我的函数仅对指定数量的参数进行类型检查,例如如果函数是用 2 个参数调用的,那么 function(...args:[...args:[string,{():void}]) 或者如果用 3,那么......或者当用 5 个参数调用时,这是一个错误。我不确定我是否没有错过这里的重点......附带问题 - 你写for(let value of values){}而不是for(const value of values){}有什么原因吗?
        • const 只是表示不能通过重新赋值来改变常量的值。
        猜你喜欢
        • 2019-11-10
        • 1970-01-01
        • 1970-01-01
        • 2018-06-18
        • 1970-01-01
        • 2017-09-17
        • 2020-12-02
        • 1970-01-01
        • 2019-03-31
        相关资源
        最近更新 更多