Typescript 中奇怪的类型推断行为

Posted

技术标签:

【中文标题】Typescript 中奇怪的类型推断行为【英文标题】:Weird Type Inference Behaviour in Typescript 【发布时间】:2020-08-12 06:07:13 【问题描述】:

假设这段代码:

// 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 类型,返回类型未解析,因此x 假定为unknown 类型,这会导致另一个错误。

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

【问题讨论】:

你在这里使用重载是有原因的吗?这两个调用签名在参数数量或返回类型上没有区别,因此&lt;I, O&gt;(m: Func&lt;I, O&gt;): Pin&lt;O, I&gt; 类型的单个调用签名就足够了(与实现签名相同的类型)。我并不是说删除重载可以解决所有推理问题(你不能在推理泛型类型参数的同时依赖函数参数的推理),但它会使问题变得更简单。 不使用重载,所有推论都不起作用,即两个提到的错误都会发生。 我认为这里的主要问题之一是x =&gt; x + 1 的值可以分配给SyncFunc&lt;number, number&gt;AsyncFunc&lt;number, number&gt;,因为this 和this。重载并没有真正的帮助。他们只是改变它首先尝试的那个。除非您有任何悬而未决的问题,否则我可以将其写下来作为答案。 你的意思是i =&gt; i * 2?好吧,它并不真正匹配 AsyncFunc&lt;number, number&gt;,因为它的返回类型不是 void(而是一个数字)。 我的意思是,i =&gt; i * 2x =&gt; x + 1 都在里面,所以不管你怎么想。尝试写出const f: AsyncFunc&lt;number, number&gt; = i =&gt; i * 2,看看它如何不会导致错误。我之前评论中的两个链接解释了这一点;你看过他们吗? 【参考方案1】:

您遇到的主要问题是x =&gt; x + 1i =&gt; i * 2SyncFunc&lt;number, number&gt;AsyncFunc&lt;number, number&gt; 之类的值的类型兼容性可能令人惊讶:

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

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

问:当x =&gt; x + 1 接受一个参数而后者需要两个参数时,如何将x =&gt; x + 1 分配给AsyncFunc? How could a function of one parameter be assignable to a function of two parameters?

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

问:当前者返回number(假设x 被推断为number)而后者返回void 时,x =&gt; x + 1 怎么能分配给AsyncFunc? How could a function returning a value be assignable to one that doesn't?

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


有鉴于此,这种行为是可以理解的;编译器无法可靠地区分AsyncFunc&lt;number, number&gt;SyncFunc&lt;number, number&gt;。要做到这一点,您可能应该将类型更改为不兼容。一种方法是使AsyncFunc 的返回类型不同于void,例如undefined

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

它们是相似的,因为没有return someValue 语句的函数最终将返回undefined,但是如果您在AsyncFunc 中返回1 而不是undefined,编译器现在会不高兴。

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

不过,现在,您已经找到了一种可以按照您想要的方式运行的解决方法:使用overloads 来引导编译器首先认真努力将x =&gt; x + 1 解释为AsyncFunc,让它失败(因为不兼容的返回类型)然后尝试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

【讨论】:

附注实际上将返回类型更改为undefined 会导致编译器对回调不返回任何值感到不满,这很奇怪。但是,将返回类型更改为 void | undefined 出人意料地解决了这个问题,因为编译器不再需要返回语句,而是正确地推断出所需的泛型。

以上是关于Typescript 中奇怪的类型推断行为的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript:使用可变元组类型进行依赖类型推断

了解条件类型的类型推断

TypeScript 学习笔记 — 类型推断和类型保护

条件类型中的 TypeScript 类型推断

TypeScript类型检查机制

TypeScript:推断嵌套联合类型的类型