TS 中的类型推断与放宽实例详解

目录
  • 简介
  • 类型推断与放宽概念
  • 常规类型推断
  • 最佳通用类型
  • 按上下文归类
  • 类型放宽
    • 常规类型放宽
    • 非严格类型检查模式
    • 严格类型检查模式
    • 字面量类型放宽
    • 对象、数组字面量类型的放宽
    • 类字面量类型的放宽
    • 函数返回值字面量类型的放宽
    • 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 | Blist2 所有可能的类型有 A、B、Base,但是存在[[子类型兼容性]]: A <- BaseB <- 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

类型放宽分为:常规类型放宽、字面量类型放宽两类,见下文。

常规类型放宽

undefinednull 类型会被编译器放宽为 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类型推断与放宽的资料请关注我们其它相关文章!

(0)

相关推荐

  • 为Vue3 组件标注 TS 类型实例详解

    目录 为 props 标注类型 使用 <script setup> 非 <script setup> 为 emits 标注类型 使用 <script setup> 非 <script setup> 为 ref() 标注类型 默认推导类型 通过接口指定类型 通过泛型指定类型 为 reactive() 标注类型 默认推导类型 通过接口指定类型 为 computed() 标注类型 默认推导类型 通过泛型指定类型 为事件处理函数标注类型 为 provide / in

  • UMD的包导出TS 类型方法示例

    目录 TypeScript 里声明模块 类型提示检查 UMD 的 global 类型 总结 TypeScript 里声明模块 在 TypeScript 里声明模块,最早是用 namespace 和 module 的语法,后来支持了 es module,类型和变量会用 import 来导入.用 export 导出. 比如你写了一个库,导出的变量叫 Guang,它下面有 name 和 age 两个属性,所以你是这样声明类型的: export default Guang; declare namesp

  • TS 类型兼容教程示例详解

    目录 类型兼容 简单类型兼容 普通对象兼容 函数兼容 参数数量不一致 参数类型不一致 返回不同 类型兼容 因为JS语言不慎过于领过, 真实开发场景中往往无法做到严格一致的类型约束,此时TS就不得不做类型兼容 顶类型:unknown -- 任何类型都可以赋值给unknown 底类型:never -- never兼容任何类型(可以赋值给任何类型) any: 其实不是一个类型,它是一个错误关闭器,用了any就等同于放弃了类型约束 简单类型兼容 子集可以赋值给父级 type name = string

  • 前端React Nextjs中的TS类型过滤实用技巧

    目录 自我介绍 分步介绍 开胃小菜 keyof in Conditional 泛型 正餐开始 实战应用例子 最后 大家好,我是零一,相信大家在阅读同事写的代码或者优秀的开源库的代码时,一定见过各种各样的风骚的TS写法,不花点时间下去根本看不懂,换作是我们,可能就直接一个 any 完事了,但是真正当项目体积变大后,你会发现这些 TS骚操作真的很重要,因为它能很好地帮助你做静态类型校验 自我介绍 TS类型过滤,英文名(我自己取的)叫 FilterConditionally,这是它完整的样子 type

  • vue3+ts中ref与reactive指定类型实现示例

    目录 ref 的基础特性 如何在ref中指定类型 reactive isRef.isReactive toRef.toRefs.toRaw ref 的基础特性 ref 约等于 reactive({ value: x }) ref() 可以定义时无参数,第一次赋值任意类型,然后就不能增加属性 const refa = ref(6) const rcta = reactive({ value: 12 }) console.log('refa:', refa) //RefImpl{...} conso

  • ts 类型体操 Chainable Options 可链式选项示例详解

    目录 问题 答案 传参 option部分 get 问题 在JavaScript我们通常会使用到可串联(Chainable/Pipline)的函数构造一个对象,但是在Typescript中,你能合理地给它赋上类型吗? 题目是: 可以使用任何你喜欢的方式实现这个类型 - interface, type, 或者 class 都行.你需要提供两个函数option(key, value) 和 get() 在 option 中你需要使用提供的key和value来扩展当前的对象类型,通过 get()获取最终结

  • TS 中的类型推断与放宽实例详解

    目录 简介 类型推断与放宽概念 常规类型推断 最佳通用类型 按上下文归类 类型放宽 常规类型放宽 非严格类型检查模式 严格类型检查模式 字面量类型放宽 对象.数组字面量类型的放宽 类字面量类型的放宽 函数返回值字面量类型的放宽 TS 内部类型放宽规则 实例分析 开篇问题解答 简介 我们知道在编码时即使不标注变量类型,TypeScript 编译器也能推断出变量类型,那 TypeScript 编译器是怎么进行类型推断,在类型推断时又是如何判断兼容性的呢? 此文,正好为你解开这个疑惑的,掌握本文讲解的

  • java 中 String format 和Math类实例详解

    java 中 String format 和Math类实例详解 java字符串格式化输出 @Test public void test() { // TODO Auto-generated method stub //可用printf(); System.out.println(String.format("I am %s", "jj")); //%s字符串 System.out.println(String.format("首字母是 %c",

  • C++ 中引用与指针的区别实例详解

    C++ 中引用与指针的区别实例详解 引用是从C++才引入的,在C中不存在.为了搞清楚引用的概念,得先搞明白变量的定义及引用与变量的区别,变量的要素一共有两个:名称与空间. 引用不是变量,它仅仅是变量的别名,没有自己独立的空间,它只符合变量的"名称"这个要素,而"空间"这个要素并不满足.换句话说,引用需要与它所引用的变量共享同一个内存空间,对引用所做的改变实际上是对所引用的变量做出修改.并且引用在定义的时候就必须被初始化.     参数传递的类型及相关要点: 1 按值

  • Angular中$cacheFactory的作用和用法实例详解

    先说下缓存: 一个缓存就是一个组件,它可以透明地储存数据,以便以后可以更快地服务于请求.多次重复地获取资源可能会导致数据重复,消耗时间.因此缓存适用于变化性不大的一些数据,缓存能够服务的请求越多,整体系统性能就能提升越多. $cacheFactory介绍: $cacheFactory是一个为Angular服务生产缓存对象的服务.要创建一个缓存对象,可以使用$cacheFactory通过一个ID和capacity.其中,ID是一个缓存对象的名称,capacity则是描述缓存键值对的最大数量. 1.

  • C++ 中引用和指针的关系实例详解

    C++ 中引用和指针的关系实例详解 1.引用在定义时必须初始化,指针没有要求 int &rNum; //未初始化不能通过编译 int *pNum; //可以 2. 一旦一个引用被初始化为指向一个对象,就不能再指向 其他对象,而指针可以在任何时候指向任何一个同类型对象 int iNum = 10; int iNum2 = 20; int &rNum = iNum; &rNum = iNum2; //不能通过 3. 没有NULL引用,但有NULL指针. int *pNum = NULL

  • Java中JDBC实现动态查询的实例详解

    一 概述 1.什么是动态查询? 从多个查询条件中随机选择若干个组合成一个DQL语句进行查询,这一过程叫做动态查询. 2.动态查询的难点 可供选择的查询条件多,组合情况多,难以一一列举. 3.最终查询语句的构成 一旦用户向查询条件中输入数据,该查询条件就成为最终条件的一部分. 二 基本原理 1.SQL基本框架 无论查询条件如何,查询字段与数据库是固定不变的,这些固定不变的内容构成SQL语句的基本框架,如 select column... from table. 2.StringBuilder形成D

  • Java 中DateUtils日期工具类的实例详解

    Java 中DateUtils日期工具类的实例详解 介绍 在java中队日期类型的处理并不方便,通常都需要借助java.text.SimpleDateFormat类来实现日期类型 和字符串类型之间的转换,但是在jdk1.8之后有所改善,jdk1.7以及之前的版本处理日期类型并不方便, 可以借助Joda Time组件来处理,尤其是日期类型的一些数学操作就更是不方便. java代码 /** * * 日期工具类 java对日期的操作一直都很不理想,直到jdk1.8之后才有了本质的改变. * 如果使用的

  • Java 中HttpURLConnection附件上传的实例详解

    Java 中HttpURLConnection附件上传的实例详解 整合了一个自己写的采用Http做附件上传的工具,分享一下! 示例代码: /** * 以Http协议传输文件 * * @author mingxue.zhang@163.com * */ public class HttpPostUtil { private final static char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJK

  • java中 String和StringBuffer的区别实例详解

    java中 String和StringBuffer的区别实例详解 String: 是对象不是原始类型.            为不可变对象,一旦被创建,就不能修改它的值.            对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.            String 是final类,即不能被继承. StringBuffer: 是一个可变对象,当对他进行修改的时候不会像String那样重新建立对象            它只能通过构造函数来建立,  

  • js中自定义react数据验证组件实例详解

    我们在做前端表单提交时,经常会遇到要对表单中的数据进行校验的问题.如果用户提交的数据不合法,例如格式不正确.非数字类型.超过最大长度.是否必填项.最大值和最小值等等,我们需要在相应的地方给出提示信息.如果用户修正了数据,我们还要将提示信息隐藏起来. 有一些现成的插件可以让你非常方便地实现这一功能,如果你使用的是knockout框架,那么你可以借助于Knockout-Validation这一插件.使用起来很简单,例如我下面的这一段代码: ko.validation.locale('zh-CN');

随机推荐