类型推断

TypeScript 程序中,每一个表达式都具有一种类型,表达式类型的来源有以下两种:

  • 类型注解。

  • 类型推断。

类型注解是最直接地定义表达式类型的方式,而类型推断是指在没使用类型注解的情况下,编译器能够自动地推导出表达式的类型。在绝大部分场景中,TypeScript 编译器都能够正确地推断出表达式的类型。类型推断在一定程度上简化了代码,避免了在程序中为每一个表达式添加类型注解。

常规类型推断

当程序中声明了一个变量并且给它赋予了初始值,那么编译器能够根据变量的初始值推断出变量的类型。示例如下:

let x = 0;
//  ~
//  推断类型为:number

此例中,变量 x 的初始值为数字 0,因此编译器推断出变量 x 的类型为 number

如果我们声明了一个常量,那么编译器能够推断出更加精确的类型。示例如下:

const x = 0;
//    ~
//    推断类型为:数字字面量类型0

此例中,我们使用 const 关键字声明了一个常量,它的值为数字 0。因为常量的值在初始化后不允许修改,因此编译器推断出常量 x 的类型为数字字面量类型 0,它比 number 类型更加精确。

如果声明变量时没有设置初始值,那么编译器将推断出变量的类型为 any 类型。示例如下:

let x;
//  ~
//  推断类型为:any

推断函数返回值的类型是另一个典型的类型推断场景,编译器能够根据函数中的 return 语句来推断出函数的返回值类型。示例如下:

function f() {  // 推断返回值类型为:number
    return 0;
}

此例中,编译器能够根据函数 f 的返回值 0 推断出函数的返回值类型为 number 类型。同时,如果将函数 f 的返回值赋值给一个变量,编译器也能够推断出该变量的类型。示例如下:

function f() {  // 推断返回值类型为:number
    return 0;
}

let x = f();
//  ~
//  推断类型为:number

此例中,编译器能够推断出函数 f 的返回值为 number 类型,进而推断出变量 x 的类型为 number 类型。

最佳通用类型

在编译器进行类型推断的过程中,有可能推断出多个可能的类型。例如,有如下的数组定义,该数组中既有 number 类型的元素,也有 string 类型的元素。编译器在推断数组的类型时,会参考每一个数组元素的类型。因此,编译器最终推断出的数组类型为联合类型 number | string。示例如下:

let x = [0, 'one'];  // (number | string)[]

此例中,每一种可能的数组元素类型都会作为类型推断的候选类型。编译器会从所有的候选类型中计算出最佳通用类型作为类型推断的结果类型。此例中,number 类型和 string 类型的最佳通用类型是联合类型 number | string,因为这两个类型之间没有子类型关系。

下面的例子能够更好地体现最佳通用类型的计算。此例中,zoo 数组有三个元素,分别为 DogCatAnimal 类的实例对象。其中,Dog 类和 Cat 类是 Animal 类的子类。最终,编译器推断出来的 zoo 数组的类型为 Animal[] 类型。因为 DogCat 的类型是 Animal 类型的子类型,因此 Animal 类型是最佳通用类型。示例如下:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const zoo = [new Dog(), new Cat(), new Animal()];
//    ~~~
//    推断类型为:Animal[]

但如果 zoo 数组中只包含了 DogCat 类的实例对象,而没有包含 Animal 类的实例对象,那么推断出来的 zoo 数组类型为联合类型 (Dog | Cat)[]。这是因为最佳通用类型算法只会从候选类型中做出选择。如果数组中没有 Animal 类型的元素,那么候选类型中只有 Dog 类型和 Cat 类型,最佳通用类型算法只会从这两种类型中做出选择。示例如下:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const zoo = [new Dog(), new Cat()];
//    ~~~
//    推断类型为:(Dog | Cat)[]

如果编译器自动推断出来的类型不是我们想要的类型,那么可以给表达式添加明确的类型注解或者使用类型断言。示例如下:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const zoo0: Animal[] = [new Dog(), new Cat()];

const zoo1 = [new Dog(), new Cat()] as Animal[];

按上下文归类

在常规类型推断中,编译器能够在变量声明语句中由变量的初始值类型推断出变量的类型。这是一种由右向左或者自下而上的类型推断,如下所示:

image 2024 05 17 11 19 49 045

反过来,编译器还能够由变量的类型来推断出变量初始值的类型。这是一种由左向右或者自上而下的类型推断。我们将这种类型推断称作按上下文归类,如下所示:

image 2024 05 17 11 20 17 285

下例中,AddFunction 接口带有调用签名,因此它表示函数类型。我们声明了 AddFunction 类型的常量 add,其类型是使用类型注解明确定义的。常量 add 的初始值是箭头函数 (x, y) => x + y。编译器能够由 AddFunction 类型推断出箭头函数中参数 xy 的类型以及其返回值类型均为 number 类型。示例如下:

interface AddFunction {
    (a: number, b: number): number;
}

const add: AddFunction = (x, y) => x + y;

在常规类型推断一节中,我们介绍了使用类型注解来 修正 Animal 数组的类型推断结果,这正是按上下文归类的应用。示例如下:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const zoo: Animal[] = [new Dog(), new Cat()];

此例第 5 行,如果没有给常量 zoo 添加类型注解 Animal[],那么推断出来的常量 zoo 的类型为 (Dog | Cat)[]。当我们给常量 zoo 添加了类型注解 Animal[] 后,由于按上下文归类的作用,Animal 类型也成了类型推断的候选类型之一。因此,由 Animal 类型、Dog 类型和 Cat 类型计算得出的最佳通用类型为 Animal 类型。