在很多情况下,TypeScript 会根据上下文环境自动推断出变量的类型,无须我们再写明类型注解。例如:
let str = 'string'
let num = 1
let bool = true
// 两个块级作用域
const str = 'string'
const num = 1
const bool = true
但是这两个示例不完全等价,你可以在 VsCode 中将鼠标 hover 在六个数上看看区别,前三个分别是 str: string
、num: number
、bool: boolean
,后三个分别是str: "string"
、num: 1
、bool: true
。具体原因我们后面再看。
TypeScript 这种基于赋值表达式推断类型的能力称之为 “类型推断”。
类型推断
在 TypeScript 中,类型标注声明是在变量之后(即类型后置),它不像 Java 语言一样,先声明变量的类型,再声明变量的名称。
使用类型标注后置的好处是编译器可以通过代码所在的上下文推导其对应的类型,无须再声明变量类型,示例如下:
let a = 42; // 推断出 a 的类型是 number
let a1: number = a; // 没问题
在上述代码中,a 的类型被推断为 number,将变量赋值给 number 类型的变量 a1 后,不会出现错误。
在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。比如我们能根据 return 语句推断函数返回的类型,如下代码所示:
/* 根据参数的类型,推断出返回值的类型也是 number */
function addFunc1(a: number, b: number) {
return a + b;
}
const x1 = addFunc1(1, 1); // 推断出 x1 的类型也是 number
/* 推断参数 b 的类型是 number 或者 undefined,返回值的类型也是数字 */
function addFunc2(a: number, b = 1) {
return a + b;
}
const x2 = addFunc2(1); // 没问题
const x3 = addFunc2(1, '1'); // error TS2345: Argument of type '"1"' is not assignable to parameter of type 'number | undefined'.
在上述 addFunc1 函数中,return 了变量 a + b 的结果,因为 a 和 b 的类型为 number,所以函数返回类型被推断为 number。
上述 addFunc2 函数中,b 参数被推断为 number | undefined 类型,如果给 b 参数传入一个字符串类型的值,由于函数参数类型不一致,此时编译器就会抛出一个 ts(2345) 错误。
上下文推断
通过类型推断的例子,我们发现变量的类型可以通过被赋值的值进行推断。除此之外,在某些特定的情况下,我们也可以通过变量所在的上下文环境推断变量的类型,具体示例如下:
type Adder = (a: number, b: number) => number;
const add: Adder = (a, b) => {
return a + b;
}
const x1 = add(1, 1); // 推断出 x1 的类型是 number
const x2 = add(1, '1'); // error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
这里我们定义了一个实现加法功能的函数类型 Adder(定义的 Adder 类型使用了 type 类型别名,后续章节会讲),声明了 add 变量的类型为 Adder 并赋值一个匿名箭头函数,箭头函数参数 a 和 b 的类型和返回类型都没有显式声明。
TypeScript 通过 add 的类型 Adder 反向推断出箭头函数参数及返回值的类型,也就是说函数参数 a、b,以及返回类型在这个变量的声明上下文中被确定了。
正是得益于 TypeScript 这种类型推导机制和能力,使得我们无须显式声明,即可直接通过上下文环境推断出变量的类型,也就是说此时类型可缺省。
字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:
let str: 'is string' = 'is string';
let num: 1 = 1;
let bool: true = true;
字面量类型是集合类型的子类型,它是集合类型的一种更具体的表达。比如 'is string'
(这里表示一个字符串字面量类型)类型是 string
类型(确切地说是 string
类型的子类型),而 string 类型不一定是 'is string'
(这里表示一个字符串字面量类型)类型,如下具体示例:
let str1: 'is string' = 'is string';
let str2: string = 'string';
str1 = str2; // error TS2322: Type 'string' is not assignable to type '"this is string"'.
str2 = str1; // 没问题
字符串字面量类型
一般来说,我们可以使用一个字符串字面量类型作为变量的类型,如下所示:
let str: 'is string' = 'is string';
str = 'is this a string'; // error TS2322: Type '"is this a string"' is not assignable to type '"is string"'.
实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合。
如下代码所示,我们使用字面量联合类型描述了一个明确、可 'left'
可 'right'
的集合,这样就能清楚地知道需要的数据结构了。
type Direction = 'left' | 'right'
function moveFunc(dir: Direction) {
......
}
moveFunc('left'); // 没问题
moveFunc('up'); // error TS2345: Argument of type '"up"' is not assignable to parameter of type 'Direction'.
通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。
因此,相较于使用 string 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型。
数字字面量类型及布尔字面量类型
数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 Config:
interface Config {
size: 'small' | 'big';
isEnable: true | false;
padding: 0 | 4 | 8;
}
在上述代码中,我们限定了 size 属性为字符串字面量类型 ‘small’ | ‘big’,isEnable 属性为布尔字面量类型 true | false(布尔字面量只包含 true 和 false,true | false 的组合跟直接使用 boolean 没有区别),padding 属性为数字字面量类型 0 | 4 | 8。
介绍完三种字面量类型后,我们再来看看通过 let 和 const 定义的变量的值相同,而变量类型不一致的具体原因。
我们先来看一个 const 示例,如下代码所示:
const str = 'is string'; // str: 'is string'
const num = 1; // num: 1
const bool = true; // bool: true
在上述代码中,我们将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定,这也是一种比较合理的设计。
接下来我们看看如下所示的 let 示例:
let str = 'is string'; // str: string
let num = 1; // num: number
let bool = true; // bool: boolean
在上述代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如 str 的类型是 ‘is string’ 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。
这种设计符合编程预期,意味着我们可以分别赋予 str 和 num 任意值(只要类型是 string 和 number 的子集的变量):
str = 'anything';
num = 777;
bool = false;
我们将 TypeScript 的字面量子类型转换为父类型的这种设计称之为 “literal widening”,也就是字面量类型的拓宽。
Literal Widening
所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
下面通过字符串字面量的示例来理解一下字面量类型拓宽:
let str = 'is string'; // 类型是 string
let strFunc = (str = 'is string') => str; // 类型是(str?: string) => string
const str1 = 'is this string'; // 类型是 'is this string'
let str2 = str1; // 类型是 string
let strFunc2 = (str = str1) => str; // 类型是 (str?: string) => string
因为第 1~2 行满足了 let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为 string(形参类型确切地讲是 string | undefined)。
因为第 3 行的常量不可变更,类型没有拓宽,所以 str1 的类型是 ‘is this string’ 字面量类型。
第 4~5 行,因为赋予的值 str1 的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2 的类型被推断为 ‘is this string’,它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。
基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为:
const str: 'is string' = 'is string'; // 类型是 'is string'
let str2 = str; // 即便使用 let 定义,类型是 'is string'
实际上,除了字面量类型拓宽之外,TypeScript 对某些特定类型值也有类似 “Type Widening” (类型拓宽)的设计
Type Widening
比如对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
const z = null; // 类型是 null
let func = (param = null) => param; // 形参类型是 null
let z2 = z; // 类型是 null
let x2 = x; // 类型是 null
let y2 = y; // 类型是 null
注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成“any”
在现代 TypeScript 中,以上示例的第 1~2 行的类型拓宽更符合实际编程习惯,我们可以赋予任何其他类型的值给具有 null 或 undefined 初始值的变量 x 和 y。
示例第 4~7 行的类型推断行为因为开启了 strictNullChecks=true
,可以让我们更谨慎对待这些变量、形参
既然有类型拓宽,自然也会有类型缩小,下面简单介绍一下 Type Narrowing
Type Narrowing
在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 “Type Narrowing”。
比如,我们可以使用类型守卫将函数参数的类型从 any 缩小到明确的类型,具体示例如下:
let func = (anything: any) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else if (typeof anything === 'number') {
return anything; // 类型是 number
}
return null;
};
同样,我们可以使用类型守卫将联合类型缩小到明确的子类型,具体示例如下:
let func = (anything: string | number) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else {
return anything; // 类型是 number
}
}
当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:
type Goods = 'pen' | 'pencil' | 'ruler';
const getPenPrice = (item: 'pen') => 2;
const getPencilPrice = (item: 'pencil') => 4;
const getRulerPrice = (item: 'ruler') => 6;
const getPrice = (item: Goods) => {
if (item === 'pen') {
return getPenPrice(item); // item => 'pen'
} else if (item === 'pencil') {
return getPencilPrice(item); // item => 'pencil'
} else {
return getRulerPrice(item); // item => 'ruler'
}
}
在上述 getPrice 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if 语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。
那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenPrice、getPencilPrice、getRulerPrice呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === ‘pencil’ 的分支,item 的类型就被收缩为“pencil”。
事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:
const getPrice = (item: Goods) => {
if (item === 'pen') {
item; // item => 'pen'
} else {
item; // item => 'pencil' | 'ruler'
}
}