Typescript 中奇怪的类型推断行为

Weird Type Inference Behaviour in Typescript

假设此代码:

// base class:
class Pin<Output, Input=Output> {
  to<Out>(pin: Pin<Out, Output>): Pin<Out, Output> {
    return pin;
  }

  from<In>(pin: Pin<Input, In>): Pin<Input, In> {
    return pin;
  }
}

//some type aliasing for convenience:
type SyncFunc<I, O> = (i: I) => O;
type Resolve<T> = SyncFunc<T, void>;
type AsyncFunc<I, O> = (i: I, cb: Resolve<O>) => void;
type Func<I, O> = SyncFunc<I, O> | AsyncFunc<I, O>;

function src<Type>(): Pin<Type> { return new Pin<Type>(); }

function map<I, O>(m: SyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: AsyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O> | AsyncFunc<I, O>): Pin<O, I> {
  return new Pin<O, I>();
}

使用此设置,以下代码将出错:

src<number>().to(map((i, c: Resolve<number>) => c(i * 2))).to(map(x => x + 1));

由于未正确推断 i 的类型。

我可以像这样更改重载签名安排:

function map<I, O>(m: AsyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O> | AsyncFunc<I, O>): Pin<O, I> {
  return new Pin<O, I>();
}

这修复了上一个示例的问题,但导致了这个问题:

src<number>().to(map(i => i * 2)).to(map(x => x + 1));

因为传递给 map() 的函数现在被假定为 AsyncFunc 类型,return 类型未被解析,因此 x 被假定为类型 unknown,这会导致另一个错误。

想在 Typescript 的 GitHub 上开一个问题,但想先在这里问一下,以确保我没有遗漏任何东西。这是预期的行为吗?即它是 Typescript 类型推断的错误,还是它目前缺少的功能,或者我是否遗漏了什么?

您 运行 遇到的主要问题是 x => x + 1i => i * 2 等值与 both[=94= 可能令人惊讶的类型兼容性] SyncFunc<number, number>AsyncFunc<number, number>:

const sf: SyncFunc<number, number> = x => x + 1; // okay
const af: AsyncFunc<number, number> = x => x + 1; // also okay... what?!

这是按预期工作的,但让足够多的人感到困惑,因为它是 TypeScript FAQ 的一部分。我会根据这个问题调整那里的信息:

问:x => x + 1如何赋值给AsyncFunc,前者接受一个参数,而后者需要两个参数? How could a function of one parameter be assignable to a function of two parameters?

A:x => x + 1AsyncFunc 的有效赋值,因为它可以安全地忽略额外参数。在运行时,(x => x + 1)(10, "randomExtraParam") 有效。在编译时将其作为一个错误最终会使许多方法的常规使用无效,例如 Array.forEach()Array.map(),这些方法的实现将多个参数传递给回调,但通常与单个参数的回调一起使用。您可以阅读上面链接的常见问题条目,了解为什么他们认为这是最好的行为。

问:当return是number时,x => x + 1如何赋值给AsyncFunc(假设x被推断为number) 和后者 returns voidHow could a function returning a value be assignable to one that doesn't?

A: void return 类型意味着调用者不能 期望 一个 return 值,但这并不意味着绝对不会有。它只是意味着调用者应该忽略任何 returned 的值。如果你忽略了我给你的任何 return 值,那么我 return 1 而不是 return undefined 应该无关紧要。将此作为一个错误最终会使许多不参考回调的 return 值的回调接受函数的常规使用无效,例如 Array.forEach()。像 a => arr.push(a) 这样的一些回调有副作用和 return 值,并且禁止 return 值会使以常规方式使用它们变得更加困难。


有鉴于此,这种行为是可以理解的;编译器无法可靠地区分 AsyncFunc<number, number>SyncFunc<number, number>。要做到这一点,您可能应该将类型更改为不兼容。一种方法是将 AsyncFunc 的 return 类型设为 void 以外的类型,例如 undefined:

type Resolve<T> = SyncFunc<T, undefined>;
type AsyncFunc<I, O> = (i: I, cb: Resolve<O>) => undefined;

它们很相似,因为没有 return someValue 语句的函数将以 returning undefined 结束,但如果你 return 编译器现在会不高兴1 而不是 AsyncFunc.

中的 undefined

这还是没有解决按参数个数区分函数的问题。如果你只是把 map() 的调用签名写成 <I, O>(m: Func<I, O>): Pin<O, I>,从技术上讲,编译器有足够的信息可以看出 x => x + 1 一定是 SyncFunc,但显然信息太多了立即推断:它需要推断类型参数 Ix 的类型,但它们相互依赖并且它放弃了。有时您可以让它工作,但并不总是值得付出努力。有关当前类型推理算法的局限性和改进它的可能性的问题和讨论,请参阅 microsoft/TypeScript#30134

但是现在,您已经有了一个按照您想要的方式运行的解决方法:使用 overloads 引导编译器首先认真努力将 x => x + 1 解释为 AsyncFunc,让它失败(因为不兼容的 return 类型)然后尝试 SyncFunc 并成功。这与之前关于 Ix 的问题相同,但类型 SyncFunc 显然足够简单,可以工作(与 Func 相比)。

这意味着以下内容适合您:

function map<I, O>(m: AsyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: Func<I, O>): Pin<O, I> {
    return new Pin<O, I>();
}

src<number>().to(map((i, c: Resolve<number>) => c(i * 2))).to(map(x => x + 1));
src<number>().to(map(i => i * 2)).to(map(x => x + 1));

好的,希望对您有所帮助;祝你好运!

Playground link to code