【问题标题】:how to add return types to range function?如何将返回类型添加到范围函数?
【发布时间】:2021-10-10 11:02:15
【问题描述】:

这段代码来自you don't know JS yet,我正在尝试为其添加类型,但我很困惑。

function range(start: number, end?: number) {
  start = Number(start) || 0;

  if (end === undefined) {
    return function getEnd(end: number) {
      return getRange(start, end);
    };
  } else {
    end = Number(end) || 0;
    return getRange(start, end);
  }

  // **********************

  function getRange(start: number, end: number) {
    var ret: number[] = [];
    for (let i = start; i <= end; i++) {
      ret.push(i);
    }
    return ret;
  }
}

range(3, 3); // [3]
range(3, 8); // [3,4,5,6,7,8]
range(3, 0); // []

var start3 = range(3);
var start4 = range(4);

start3(3); // [3]
start3(8); // [3,4,5,6,7,8]
start3(0); // []

start4(6); // [4,5,6]

错误信息是:

此表达式不可调用。 并非所有类型的成分 '((end: number) => number[]) | number[]' 是可调用的。 类型 'number[]' 没有调用签名.ts(2349)

我正在努力寻找答案,但还是不明白。我希望你能帮助我。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    编译器查看了您的return 语句并推断 range 的返回类型是union type ((end: number) =&gt; number[]) | number[]。因此,当您调用range() 时,编译器只知道它返回另一个函数 一个数字数组。 如何你调用range()这两种类型中的哪个返回值实际上是:

    const r33 = range(3, 3); // [3]
    // const r33: ((end: number) => number[]) | number[]
    
    var start3 = range(3);
    // var start3: ((end: number) => number[]) | number[]
    

    看到了吗? r33start3 都被视为同一类型,可能是一个数组,但 可能是一个函数。它看不出start3(3)r33(3)之间的区别。

    所以如果你调用start3(3),编译器会警告你它不能确定你没有像调用函数一样调用数组。除非您在调用它之前明确检查 start3

    var start3 = range(3);
    if (typeof start3 !== "function") throw new Error();
    
    start3(3); // okay
    start3(8); // okay
    start3(0); // okay
    

    或者除非你只是 assert 它是一个函数:

    (start4 as Extract<typeof start4, Function>)(6); // okay
    

    这两者都不是你想做的。


    因此编译器无法真正推断输入数量和输出类型之间的关系。但是有办法告诉编译器这个信息,这样调用者就可以更轻松了。

    也许最直接的方法是将range() 变成overloaded function。您首先提供一组调用签名声明,对应于调用者可以预期的不同的不同输入-输出关系:

    // call signatures
    function range(start: number, end: number): number[];
    function range(start: number): (end: number) => number[];
    

    然后你像以前一样编写实现:

    function range(start: number, end?: number) {
      // same impl here
    }
    

    这编译得很好,尽管编译器并没有非常彻底地检查它。例如,如果我在这些调用签名中混合了两种返回类型(将number[] 放在第二个上,将(end: number) =&gt; number[] 放在第一个上),编译器就不会捕捉到这样的错误。重载函数实现的检查比普通函数实现更宽松,所以你需要小心。

    不管怎样,现在调用者可以调用range 并且有更合理的行为:

    const r33 = range(3, 3); // [3]
    // const r33: number[]
    
    var start3 = range(3);
    // var start3: (end: number) => number[]
    

    现在编译器知道r33 是一个数组,start3 是一个函数。之后的调用将按预期工作。


    另一种方法是使range() 成为generic 函数,其返回类型为conditional type,这取决于函数的调用方式:

    function range<T extends [end: number] | []>(start: number, ...[end]: T):
      T extends [] ? (end: number) => number[] : number[] {
      start = Number(start) || 0;
    
      if (end === undefined) {
        return function getEnd(end: number) {
          return getRange(start, end);
        } as any;
      } else {
        end = Number(end) || 0;
        return getRange(start, end) as any;
      }
    
      // same getRange impl here
    }
    

    这里我们说的是,在start 参数之后,您可以期望rest of the arguments T 可以分配给与end 参数对应的单个元素tuple,或者是一个空的元组对应没有 end 参数。然后T extends [] ? (end: number) =&gt; number[] : number[] 的返回值根据T 计算为函数类型或数组类型。

    编译器仍然无法遵循函数内部的逻辑来验证实际返回是否符合此泛型条件类型(有关更多信息,请参阅microsoft/TypeScript#33912),但与重载不同,编译器会抱怨这一点。所以现在这意味着你需要类型断言之类的东西来使返回语句在没有警告的情况下编译(参见我添加的as any),因此这个解决方案与重载一样不安全。


    无论哪种方式,您都需要注意调用签名所表达的输入/输出关系实际上是正确实现的。

    Playground link to code

    【讨论】:

    • 非常感谢你,jaclz
    猜你喜欢
    • 1970-01-01
    • 2019-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多