打字稿中的函数重载和类型推断

Function overloading and type inference in typescript

我有这种类型:

type Bar = number;
type Foo = {
   doIt: ((value: string) => Bar) | ((value: string, provideBar: (bar: Bar) => void) => void);
}

想法是 Bar 可以根据提供的函数签名以两种方式之一返回。使用 Foo 实现的代码如下所示:

function getBar(foo: Foo) {
  let bar: Bar = 0;

  if (foo.doIt.length === 1) { // We've been provided with the first version of `Foo`
    bar = foo.doIt('hello');  
    // The above line is erroring with: 
    // - bar: Type 'number | void' is not assignable to type 'number'
    // - invocation of "doIt": Expected 2 arguments, but got 1.

  } else if (foo.doIt.length === 2) { // We've been provided with the second version of `Foo`
    foo.doIt('hello', (_bar) => bar = _bar);
  }
}

提供 Foo 实现的代码如下所示:

function provideBar() {
  const foo1: Foo = {
    doIt: (value) => 1. // Error:  Parameter 'value' implicitly has an 'any' type.
  }

  const foo2: Foo = {
    doIt: (value, provideBar) => provideBar(2) // Appears to be working
  }
}

我希望打字稿能够表达我想要实现的目标。我不确定为什么会出现这些错误,因为在我看来,TS 有足够的信息来禁止提供类型推断(我假设 TS 可以使用 function.length 在两种方式之间进行区分实施 Foo)

对于 getBar() 实现中的问题,您 运行 进入 microsoft/TypeScript#18422,在 TypeScript 中被列为设计限制。

编译器只看到函数的 length 属性 是 number 类型,而不是任何特定的 numeric literal type like 1 or 2. So checking foo.doIt.length === 1 has no implication for control flow analysis,因此编译器不知道它调用的是两种函数类型中的哪一种。

检查 length 的一个主要问题是它可能不是您认为的那样。函数可以用 rest parameters 来实现,而 length 很可能是 0.

或者因为 TypeScript 允许您将接受较少参数的函数分配给接受较多参数的函数(参见 this FAQ entry),匹配 (value: string, provideBar: (bar: Bar) => void) => void 的函数可能具有 length 10 因为函数实现可以自由忽略它们的任何输入。

由于 length 如此古怪,TypeScript 基本上什么都不做,建议您不要尝试以这种方式检查 length

不过,如果您确信检查会按照您的想法进行(也就是说,没有人会将 "doIt" 设置为上面的“gotcha”版本之一),您可以通过实施来获得类似的行为user-defined type guard function:

function takesOneArg<F extends Function>(x: F): x is Extract<F, (y: any) => any> {
    return x.length === 1
}

takesOneArg() 函数检查 F 类型的类函数参数的 length 和 returns true 是否等于 1false 否则。 return 类型谓词 x is Extract<F, (y: any) => any> 意味着如果 F 是函数类型的联合,true 结果意味着 x 的类型可以缩小到F 的成员接受一个参数; false 结果意味着 x 可以缩小到其他结果。

现在您的 getBar() 实现按预期工作:

function getBar(foo: Foo) {
    let bar: Bar = 0;
    if (takesOneArg(foo.doIt)) {
        bar = foo.doIt('hello');
    } else {
        foo.doIt('hello', (_bar) => bar = _bar);
    }
}

至于当你 创建 一个 Foo 时你在回调参数中看到隐式 any 错误的问题,这似乎是 microsoft/TypeScript#37580. You would like value to be contextually typedstring,但编译器没有这样做。 GitHub 问题中没有太多信息,所以我不能说是什么导致函数联合和上下文类型推断之间的交互不佳。

假设这不会很快得到修复,解决方法与我一直推荐的隐式 any 问题相同:显式注释编译器无法推断的事情:

const foo1: Foo = {
    doIt: (value: string) => 1
}

现在编译没有错误。


Playground link to code