【问题标题】:Typescript not inferring type from outer function打字稿不从外部函数推断类型
【发布时间】:2020-11-03 04:53:16
【问题描述】:

有没有办法帮助编译器推断如下内容:

class Base<T> {
    children(... children: (A<any> | B<T>)[]) { return this }
    onVisit(handler:(context: T)=>void) { return this }
}

class A<T> extends Base<T> {
    constructor( public context: T ) { super() }
}

class B<T> extends Base<T> {}

const foo = new A({ bar: 1 })
    .children(
        new A({baz:2}).onVisit(({baz})=>{}),
        new B().onVisit(({bar})=>{}) // Fails here because it infers that the instance as type B<unknown> instead of B<{bar:number}>
    )

这似乎不是因为编译器无法从调用函数中绘制一些上下文,因为这可行:

function f1<T>(p: T, ...h: ((p:T) => void)[]) { }
function f2<T>(h: (p: T) => void) { return h }

f1({ a: 1 }, 
    f2((p) => { console.log(p.a) }),
)

我可能完全糊涂了(很可能),但似乎后者是否有效,前者也应该如此。

【问题讨论】:

  • 这是因为当您创建 new B() 时,这将创建一个 B。在查看onVisit 之前,编译器将首先对此进行分析。所以创建B时需要提供type参数。
  • 我是这么想的,但是为什么编译器不将第二个例子中的函数实例化为f2&lt;unknown&gt;呢?构造函数是否因某种原因而被区别对待,还是我误解了不同场景之间的区别?
  • 您的第一个示例中的构造函数没有参数,因此无法推断类型。在您的第二个示例中,推断出 f2 的参数,因为在 f1 的定义中,您定义了两个参数的 T 相同。
  • @StéphaneVeyret: facepalm: 对。当然。谢谢。

标签: typescript type-inference


【解决方案1】:

TypeScript 中的“正常”类型推断发生在编译器检查表达式的内容 以确定它是什么类型时。它看起来 inside 执行此操作的表达式。这往往与运行时发出的程序的控制流方向相同。如果我有类似的代码

let x = ""; 
let y = x.length;

,编译器确定"" 的类型(即""),并使用它来确定x 的类型(即string)。然后编译器通过检查string(将是number)上的length属性的类型来确定x.length的类型,然后使用它来确定y的类型(将是@ 987654334@)。前面的表达式决定后面的表达式的类型。

但是,在少数情况下编译器会执行contextual typing。在上下文类型中,当编译器检查表达式的 context 以确定它是什么类型时,就会发生推理。它看起来表达式之外执行此操作。这往往发生在运行时发出的程序的控制流的相反方向。如果我有类似的代码

[""].map(z => z.length)

,回调内部的变量z没有显式类型,但是通过检查回调的内容无法确定其类型。但是,由于 string 值数组的 map() 方法期望它的参数是一个带有 string 属性的回调,所以编译器使用这个 contextz 提供string 的类型。这里,在某种意义上,后面的表达式是用来判断前面表达式的类型的。

但是上下文类型很脆弱。它只发生在少数特殊情况下,并且很容易通过使必要的上下文信息“远离”需要推断类型的表达式来打破:

let cb = z => z.length; // error, z is implicitly any
[""].map(cb); // oops, now we have an any[]

这里同样的回调,z =&gt; z.length 正在被创建。但它正在自行发生。唯一使用它的地方是作为string[]map() 方法的回调。所以从技术上讲,编译器可以可以想象说,“好吧,cb 应该是像(val: string) =&gt; any 这样的类型的回调,因此我们向上一行并给cb 那个类型,然后从那个上下文中我们可以推断出z 必须是string。但这不会发生。推断失败。


上下文类型发生的一个地方是通过使用上下文的预期返回类型来推断调用的泛型函数的类型参数。您的f2() 情况是这样的,类似于:

declare function f<T>(): T;
const numGood: number = f(); // infers number for T

函数f() 声称返回任何可能类型的值T。人们永远无法以“正常”方式从对f() 的调用中推断出T,因为f() 并没有告诉您T 应该是什么。但就上下文而言,编译器希望它返回number,因为numGood 被注释为number。所以它起作用了。这实际上可以通过多个函数调用向后传播,只要类型映射简单明了:

declare function id<T>(x: T): T;
const numGood: number = id(id(id(id(f())))); // infers number for all Ts

但是,在您的 new B().onVisit 情况下,您正试图让编译器根据 property 的类型(或者,而是一个方法)的返回值。这显然打破了上下文推理。类似这样:

declare function g<T>(): { a: T };
const numBad: number = g().a; // error!

函数g() 返回一个{a: T} 类型的值。尽管g().a 被用于期望number 的上下文中,但这显然不会向后传播到g() 的调用站点。推理失败。 T 默认为 unknown,你会得到一个错误。


我无法指出某个特定的 GitHub 问题或文档,其中概述了您何时可以和不能期望上下文输入起作用。有一些未遂事件,例如microsoft/TypeScript#29771,它描述了一些在编译器执行上下文类型的能力限制下的情况。但我还没有看到“f2 是,new B().onVisit 否”的规范答案。

除此之外,如果您遇到类型推断失败的情况,请考虑手动注释或自己指定类型。如果您不希望new B() 生成B&lt;unknown&gt;,则编写new B&lt;{bar: number}&gt;(),它应该会开始工作。乏味?当然。但至少它有效!


Playground link to code

【讨论】:

    猜你喜欢
    • 2020-12-20
    • 1970-01-01
    • 2020-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多