TS 中的类型推断与放宽实例详解
作者:左肩有你
简介
我们知道在编码时即使不标注变量类型,TypeScript 编译器也能推断出变量类型,那 TypeScript 编译器是怎么进行类型推断,在类型推断时又是如何判断兼容性的呢?
此文,正好为你解开这个疑惑的,掌握本文讲解的类型推断与类型放宽知识点后将对 TypeScript 的类型系统有更深的认识。
不妨先看看下面几个问题,如果你都能回答上,那么可以不用阅读此文了。
- 这里变量 x 和 y 分别为什么类型,为什么?
let x = 0; const y = 0;
- 这里函数返回值、变量 x 为什么类型,为什么?
function f() { return 0 } let x = f();
- 这里 list 为什么类型,为什么?
const list = ['hello', 0];
- 这里 x、y、a、b 为什么类型,为什么?
const x = 0; let y = x; const a: 0 = 0; let b = a;
类型推断与放宽概念
我们知道 JS 中表达式都具有返回值,在 TypeScript 程序中表达式也一样具有返回值的同时还具有一种类型(返回值的类型),且此类型来源分为:类型注解、类型推断。
类型注解是通过编写代码手动指定表达式返回值的类型,如下代码:
let x: number = 0; // 通过类型注解指定变量 x 为 number 类型
类型推断指的是 TypeScript 编译器自动推测表达式返回值的类型,是一种比较智能的类型推测方法,可以简化代码,如下代码:
let x = 0; // 这里 TypeScript 编译器自动推断变量 x 为 number 类型
上面两段代码中字面量 Literal
的值明明是字面量类型 0
,但是变量 x
却变为了 number
类型。值的类型和推断的变量类型不一致,这就涉及到 TypeScript 的类型放宽了。
常规类型推断
上述代码定义了变量 x 并给其赋值了初始值,属于常规类型推断。
下面代码中,变量 x 具有初始值 0,编译器推断其类型为 number
类型。
下面代码中,变量 x 具有初始值 0,但是使用了 const
关键字定义其为常量,故编译器推断其类型为字面量类型 0
。
假如变量声明时未指定初始值呢?这时,编译器将其自动推断为 any
类型。根据[[子类型兼容性]]章节中介绍可知,any
类型属于顶端类型之一,不是任意类型的子类型,但是却与任意类型满足赋值兼容性,这样未指定初始值的变量 x 后面可以被被赋值为任意类型。
最佳通用类型
编译器在进行类型推断过程中,有可能推断出多个可能得类型,并会参考所有可能的类型得出最终的最佳通用类型。
这里得出的类型可能为字面量 hello 对应的原始类型 string、字面量 0 对应的原始类型 number,得出的最佳通用类型为 string | number
。
const list = ['hello', 0]; // (string | number)[]
这里正好解释了开篇提出的问题 3
当数组的成员类型存在子类型关系时,最佳通用类型也会有所不同。
这里 list1
根据可能的类型 A、B 得出最佳通用类型为 A | B
,list2
所有可能的类型有 A、B、Base,但是存在[[子类型兼容性]]: A <- Base
和 B <- Base
,所以得出的最佳通用类型为 Base
。
class Base { version: string = '1.0.0' } class A extends Base {} class B extends Base {} const list1 = [new A(), new B()] // (A | B)[] const list2 = [new A(), new B(), new Base()] // Base[]
代码运行验证如下:
按上下文归类
上文说的常规类型推断、最佳通用类型都是由表达式的结果推导对应变量的类型,这是一个由右向左的推断过程。TypeScript 编译器还能够由变量的类型来推导变量对应初始值的类型,这是一个由左向右的推断过程。
这里指定变量 f 为 AddFunction
类型,给定的初始值是一个函数,并且这个函数的形参和返回值都未指定类型,编译器会自动根据 f 的类型推导出初始值的形参和返回值类型。
interface AddFunction { (x: number, y: number): number; } let f: AddFunction = (x, y) => { return x + y; }
编译器按上下文归类推断出的类型如下:
类型放宽
上文在介绍最佳通用类型时提到过“字面量 hello 对应的原始类型 string”,这就属于类型放宽。编译器在进行类型推断时候会进行类型放宽,比如字面量类型 hello 放宽为原始类型 string。同样,下面变量 x 也会被放宽为 number 类型。
let x = 0; // number
类型放宽分为:常规类型放宽、字面量类型放宽两类,见下文。
常规类型放宽
undefined
和 null
类型会被编译器放宽为 any 类型,不过这一特性在配置的编译器检查规则 --strictNullChecks
不同时情况不一样。
非严格类型检查模式
修改 tsconfig.json 配置文件为如下:
{ "compilerOptions": { "strictNullChecks": false } }
let x1 = undefined; // any const x2 = undefined; // any let y1 = null; // any const y2 = null; // any
此模式下,undefined 的值依然是 undefined 类型(null 同理),只是编译器在进行类型推断时将 undefined 类型放宽为了 any 类型。
严格类型检查模式
修改 tsconfig.json 配置文件为如下:
{ "compilerOptions": { "strictNullChecks": true } }
let x1 = undefined; // undefined const x2 = undefined; // undefined let y1 = null; // null const y2 = null; // null
此模式下,编译器不会对 undefined、null 类型进行放宽,undefined 的值依然是 undefined 类型(null 同理)。
字面量类型放宽
字面量类型在进行类型推断时,若当前表达式的值是可变的,则会对字面量的类型进行放宽,放宽规则如下表。
开篇的问题 1 中的代码见下方,定义了两个表达式,之前 let 定义的表达式值是可变的,const 定义的表达式值是不可变的。因此,变量 x 类型按照字面量进行放宽为 string 类型,变量 y 类型不会进行放宽,为字面量类型 0。
let x = 0; const y = 0;
对象、数组字面量类型的放宽
上文以表达式的值是否可变的角度来看待字面量类型是否可以放宽并非十分恰当,对于使用 const 关键字定义的对象、数组的情况则稍有不同。
JS 中 const 定义的变量不可变指的是变量指向的指针不可变,但是对象、数组是引用类型,当对象的属性或数组的元素的值变化(或者指向的指针变化)时,该变量的指针并未改变。
因此,对象、数组字面量类型在进行推断时也会进行类型放宽,这正是开篇的问题 3 的解答。
下面代码 base.version
的类型会进行放宽,结果类型为:number,base.author
同样,放宽为:string。
const base = { version: 1, author: 'JohnieXu' };
下面代码 list 的类型会进行放宽,结果类型为:(string | number)[]
。
const list = ['hello', 0];
类字面量类型的放宽
类字面量和对象字面量比较相似,因为在类在 JS 中(或者说 JS 解释器)也是通过对象进行模拟的,不同仅在于类的属性具有修饰符。对于具有 readonly 修饰符的对象属性,因其值不可变,故不会进行类型放宽。
函数返回值字面量类型的放宽
在函数或方法中,若返回值的类型为字面量类型,则编译器推断的返回值类型会放宽;若返回值的类型为字面量联合类型,则不会放宽。
TS 内部类型放宽规则
每个字面量类型都有一个内置属性表示其是否可以被放宽,而 TypeScript 编译器会根据放宽规则来推断出这个内置属性。
在 TypeScript 语言内部实现中,根据字面量的来源不同进行了分类,来自于表达式的字面量类型标记为全新的(fresh)字面量类型。只有全新的字面量类型才是可放宽的字面量类型,并且根据字面量处于表达式的位置,分为:可变值位置、不可变值位置。
因此,字面量的类型可放宽的充分必要条件为:为全新的字面量类型,且在代码中处于可变值的位置。
实例分析
以开篇的问题 4 中部分代码为例:
const x = 0; let y = x;
变量 x、y 的类型见下图,可见两者类型并不相同,x 类型未放宽,y 类型有放宽。
分析过程如下:
- 分析表达式
const x = 0;
- 表达式中字面量 0 为全新的字面量类型
- 表达式中使用了 const 关键字,字面量 0 处于不可变值位置,因此推断 x 类型时不进行类型放宽
- 变量 x 的类型是:可放宽的数字字面量类型 0(全新的字面量类型 0)
- 分析表达式
let y = x;
- 表达式中变量 x 为可放宽的数字字面量类型 0
- 表达式中使用了 let 关键字,变量 x 处于可变值位置,因此推断 y 类型时进行类型放宽
- 变量 y 的类型是可放宽的数字字面量类型 0 的放宽类型,即:number 类型。
下面还是以开篇的问题 4 中部分代码为例(说明使用了类型注解的场景):
const a: 0 = 0; let b = a;
变量 a、b 的类型见下图,可见两者类型相同,都没有类型放宽。
分析过程如下:
- 分析表达式
const a: 0 = 0;
- 变量 a 的初始值 0 的类型为全新的字面量类型 0,即可放宽的字面量类型 0
- 但是,这里通过类型注解 0,指定了变量 a 的类型为字面量类型 0,由于类型注解的字面量类型不是全新的字面量类型,所以变量 a 的类型为不可放宽的字面量类型 0
- 分析表达式
let b = a;
- 这里变量 b 的初始值 a 的类型为不可放宽的字面量类型 0,虽然使用 let 关键字定义让其处于可变值位置,但是不满足类型放宽的必要条件,所以变量 b 的类型为不可放宽的字面量类型 0。
开篇问题解答
开篇提出的问题中 1、3、4 已在上文讲解过程中进行过分析,这里分析一下问题 2 。
function f() { return 0 } let x = f();
先看这个问题的答案,如下:
分析过程:
- 函数 f 的返回值类型为字面量类型 0,根据上文介绍的“函数返回值类型为字面量类型会进行类型放宽”可知,函数 f 返回值类型为字面量类型 0 放宽的结果类型:number 类型
- 分析表达式
let x = f();
- 这里变量 x 的初始值是函数 f 的返回值,是 number 类型
- 表达式采用了 let 关键字,处于可变值位置,会对 number 类型进行放宽
- number 类型放宽的结果类型为自身:number 类型,故变量 x 为 number 类型。
以上就是TS 中的类型推断与放宽实例详解的详细内容,更多关于TS类型推断与放宽的资料请关注脚本之家其它相关文章!