要完全理解类型推论需要完整理解类型上下文,并且理解TS对于是否可以使用类型推论是基于静态分析完成的。
上下文类型应用在许多地方。常见的例子包括函数调用的参数,赋值的右手端位置,类型断言,对象和数组的成员,和返回语句。上下文类型还充当最佳公共类型中的候选类型。
TS中需要为每个JS名字规定类型,而名字出现在对应的上下文中则会自动获得类型,若没有对应的上下文,这个名字则会自动获得类型any。
名字:通过声明语句声明的名字,例如var、let、const、function a() {}、class A {}、import A from ‘.a’、函数参数等,都会在JS环境中添加一个名字,而TS可以给这个名字指定类型。
在JS中名字的声明是可以在上面提到的常见例子指定的位置,函数参数调用、赋值右手端位置、对象数组成员、返回语句。
函数调用的参数
interface Cb {(a: number): void;
}
interface Fn {(cb: Cb): void;
}const fn: Fn = function (cb) {}fn(function (a) { // 这里a的类型是numberconsole.log(a + 1)
})
因为fn这个名字是类型Fn,而Fn类型的入参是类型Cb,所以在fn使用匿名函数作为入参调用的时候TS可以知道这个匿名函数对应的位置是类型Cb,换句话说匿名函数当前的类型上下文是Cb。而Cb要求入参是number类型,所以推断出匿名函数的参数a是number类型。
赋值的右手端位置
interface Cb {(a: number): void;
}const fn: Cb = function (a) {console.log(a + 1)
}
fn是类型Cb,因为Cb要求入参是number类型,所以TS推断出对应位置匿名函数的入参是number类型。
对象和数组的成员
interface Cb {(a: number): void;
}
interface Obj {fn: Cb
}
const obj: Obj = {fn: function (a) {console.log(a + 1)}
}
obj是类型Obj,而Obj具有属性fn是类型Cb。因为Cb要求入参是number类型,所以TS推断出对象obj.fn右手端对应位置匿名函数的入参是number类型。
返回语句
interface Cb {(a: number): void
}interface Fn {(): Cb
}const fn: Fn = function () {return (a) => {console.log(a + 1)}
}
在这例子里,返回的匿名函数获得类型上下文Cb,而Cb要求入参是number,所以匿名函数的入参被推断出是number类型。
上面的几种类型都可以认为名字出现在了赋值的右手端,而被复制的名字可以给这个值提供对应的类型上下文。
类型断言
interface Fn {(a: number): void;
}const fn = function (a) {console.log(a + 1);
} as Fn;
在这里匿名函数赋值给变量fn而fn本身是没有类型的,所以没办法推断匿名函数的入参a的类型,但是我们使用类型给这个匿名函数指定了类型上下文Fn,让TS具有了推算参数a的依据,得出参数a是number类型。
小结
类型推断起作用的条件是名字出现在对应的上下文位置,而这个上下文可以通过赋值操作的左手端提供,也可以使用类型断言直接提供。这样TS可以根据对应的类型推断出对应变量的类型。
一些意外
interface Fn {(a: number): void;
}function fn(a) {}let a: Fn = fn
在这个例子里,TS无法推断出函数fn的参数a是number类型,因为赋值操作提供的类型上下文在右手端,而fn这个函数声明的位置,并不是右手端。对应的右手端位置并不是函数声明,而是函数声明的引用。所以TS无法静态分析出fn中a的类型。
还有一个原因是fn的使用并不唯一,在这里我们将fn赋值给Fn类型的变量a,我们完全可以将它再赋值给Fn1类型的变量b,所以这种情况下fn中a的类型是由运行时决定的,无法静态分析出来。
interface Fn {(a: number): void;
}function fn(a) {}let a: Fn = fninterface Fn1 {(a: number): void;
}let b: Fn1 = fn
这里Fn1要求入参a的类型是string,所以fn的入参a既可能是number也可能是string,只有运行这个函数的时候才能确定知道参数a是什么类型。所以在这个例子中a会自动获得隐式类型any。
进行如下修改:
interface Fn {(a: number): void;
}let a: Fn = function fn(a) {}
在这里fn是一个表达式,并不会再当前环境中新建一个名字fn,换句话说这个fn不会再用在别的地方,并且a这个名字直接出现在了右手端对应位置,所以这个fn函数可以得到类型上下文Fn,从而推断出参数a的类型是number。