如果作为参数传递,函数签名不会自动加宽

Function signature is not automatically widened if passed as an argument

我正在尝试创建一个接受大量具有特定签名的函数的高阶函数。在这种情况下,签名是任何函数,它只接受一个实现特定接口的参数。说,Foo:

interface Foo {
    foo: string
}

function higherOrderStuff(funs: ((foo: Foo) => void)[]) {
    // do higher order stuff
}

但是,为了便于类型检查,一些函数使用了原始接口的intersections。像这样:

type MyFoo = Foo & {
    foo: 'bar'
};

function myFunction(foo: MyFoo) {
    // do my stuff
}

不幸的是,将这些函数传递给高阶包装器会产生错误:

higherOrderStuff([myFunction]); // compiler error
TS2322: Type '(foo: MyFoo) => void' is not assignable to type '(foo: Foo) => void'.
  Types of parameters 'foo' and 'foo' are incompatible.
    Type 'Foo' is not assignable to type 'MyFoo'.
      Type 'Foo' is not assignable to type '{ foo: "bar"; }'.
        Types of property 'foo' are incompatible.
          Type 'string' is not assignable to type '"bar"'.

错误是正确的:Foo 无法分配给 MyFoo。事实上,我做相反的事情是有原因的。

更奇怪的是,如果我尝试不同的东西并传递函数以外的任何东西,相同的场景会按预期工作。

function doStuffWithFoos(foos: Foo[]) {
    // do stuff
}

const myFoo: MyFoo = { foo: 'bar' };

doStuffWithFoos([myFoo]); // no squiggly line

那么,为什么参数被正确加宽,而函数参数的参数却没有?

显式转换修复了错误,但我似乎不需要它。

higherOrderStuff([myFunction as (foo: Foo) => void]); // this works

编译器错误的发生是因为函数在它们的参数类型中是逆变,这基本上意味着如果您传递的函数采用T类型的参数,您不能传递一个函数,该函数采用 T 子类型的参数(不过超类型很好)。

作为代码中的示例,假设您通过将对象 {foo: 'not-bar'} 传递给 funs:

中的每个函数来实现 higherOrderStuff
interface Foo {
  foo: string
}

function higherOrderStuff(funs: ((foo: Foo) => void)[]) {
  return funs.map(f => f({foo: 'not-bar'}))
}

由于 MyFoo 具有较窄的类型 {foo: bar} 由于

type MyFoo = Foo & {
  foo: 'bar'
};

您可以定义一个 myFunction 取决于其参数具有这种更窄的类型:

function myFunction(foo: MyFoo) {
  const shouldBeBar = foo.foo // inferred type: 'bar'
  const barObject = {bar: () => 42}
  const barFunction = barObject[shouldBeBar]
  return barFunction() // should return 42
}

一切仍然类型正确,因为 shouldBeBar 只能是 barbarFunction 将是一个返回 42 的函数。

但是如果你现在 运行

console.log(higherOrderStuff([myFunction]))

它最终会调用 myFunction({foo: 'not-bar'}),这将导致 barFunction 结束 undefined 并引发 运行 时间错误。

这是类型错误试图避免发生的事情。

TypeScript playground

要理解协变和逆变,水果和榨汁机的类比可能会有所帮助。

如果你要一块水果,得到亚型橙子就可以了,而如果你要一个橙子,你不只是想得到任何一种水果。这是协方差。

现在考虑你需要一个普通的果汁机(即从水果到果汁的功能),那么买一个橙子榨汁机就不行了,因为你可能还想榨菠萝汁。另一方面,如果您要橙子榨汁机,那么普通的果汁机就可以了。你可以说榨汁机在榨汁的类型上是逆变的。