打字稿不从外部功能推断类型

时间:2020-11-02 18:04:14

标签: typescript type-inference

有没有一种方法可以帮助编译器推断如下内容:

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) }),
)

我可能完全感到困惑(很可能),但是如果后者起作用,那么前者应该如此。

1 个答案:

答案 0 :(得分:2)

当编译器检查表达式的内容以确定其类型时,就会发生TypeScript中的

“普通”类型推断。它在表达式中 进行查找。这倾向于在运行时与发出的程序的控制流相同的方向发生。如果我有类似的代码

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

,编译器确定""的类型(将为""),并使用它来确定x的类型(将为string)。然后,编译器通过检查x.length上的length属性的类型(将为string)来确定number的类型,然后使用它来确定y(将为number)。较早的表达式确定较晚的表达式的类型。

但是,在某些情况下编译器会执行contextual typing。在上下文类型化中,推断发生在编译器检查表达式的 context 以确定其类型时。它看起来在表达式的 outside 之外。这倾向于在运行时在所发出程序的控制流的“相反”方向上发生。如果我有类似的代码

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

,回调中的变量z没有显式类型,但是无法通过检查回调的内容来确定其类型。但是,由于map()值数组的string方法期望其参数是采用string属性的回调,因此编译器使用此< em> context 赋予z类型string。在某种意义上,此处使用较晚的表达式来确定较早的表达式的类型。

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

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

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


发生上下文类型输入的一个地方是通过使用上下文的预期返回类型来推断被调用泛型函数的类型参数。您的z情况是这样做的,

string

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

numGood

但是,在您的number情况下,您尝试根据给定 property 的类型,使编译器根据上下文推断一个函数(或更确切地说,一个构造函数调用)的类型参数。或方法)的返回值。这显然破坏了上下文推断。与此类似:

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

函数new B().onVisit返回类型为declare function g<T>(): { a: T }; const numBad: number = g().a; // error! 的值。即使在期望g()的上下文中使用{a: T},它显然也不会向后传播到g().a的呼叫站点。推论失败。 number默认为g(),您会收到错误消息。


我无法指出某个特定的GitHub问题或文档,该文档或文档概述了何时以及何时不能期望上下文键入起作用。诸如microsoft/TypeScript#29771之类的遗漏之处在于,它描述了编译器执行上下文类型输入的能力受到限制的某些情况。但是我还没有看到规范的答案,说“ T是,unknown否”。

除非出现类型推断失败的情况,否则请考虑自己手动注释或指定类型。如果您不希望f2产生new B().onVisit,请写new B(),它应该开始工作。乏味?当然。但是至少可以用!


Playground link to code