TypeScript入门实战笔记 -- 04 什么是字面量类型、类型推断、类型拓宽和类型缩小?

🍍开发环境

1:使用vscode 新建一个 04.Literal.ts 文件,运行下列示例。

2:执行 tsc 04.Literal.ts --strict --alwaysStrict false --watch

3:安装nodemon( 全局安装npm install -g nodemon ) 检测.js文件变化重启项目,打印输出结果

执行:nodemon 04.Literal.js

前面我们已经学习了 TypeScript 的基本语法和基础类型,接下来通过几个例子温习一下(为了避免命名冲突,后续示例中会引入一对花括符 {} 创建块级作用域):

{let str: string = 'this is string';let num: number = 1;let bool: boolean = true;
}
{const str: string = 'this is string';const num: number = 1;const bool: boolean = true;
}

看着上面的示例,定义基础类型的变量都需要写明类型注解,TypeScript 太麻烦了吧?在示例中,使用 let 定义变量时,我们写明类型注解也就罢了,毕竟值可能会被改变。可是,使用 const 常量时还需要写明类型注解,那可真的很麻烦。

实际上,TypeScript 早就考虑到了这么简单而明显的问题。

在很多情况下,TypeScript 会根据上下文环境自动推断出变量的类型,无须我们再写明类型注解。因此,上面的示例可以简化为如下所示内容:

{let str = 'this is string'; // 等价let num = 1; // 等价let bool = true; // 等价
}
{const str = 'this is string'; // 不等价const num = 1; // 不等价const bool = true; // 不等价
}

注意:这里说的仅仅是“简化”,而不是说两个示例完全等价,接下来我们会进一步介绍。

我们把 TypeScript 这种基于赋值表达式推断类型的能力称之为“类型推断”。

🍍类型推断

在 TypeScript 中,类型标注声明是在变量之后(即类型后置),它不像 Java 语言一样,先声明变量的类型,再声明变量的名称。

使用类型标注后置的好处是编译器可以通过代码所在的上下文推导其对应的类型,无须再声明变量类型,具体示例如下:

{let x1 = 42; // 推断出 x1 的类型是 numberlet x2: number = x1; // ok
}

在上述代码中,x1 的类型被推断为 number,将变量赋值给 number 类型的变量 x2 后,不会出现任何错误。

在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型。都可以根据上下文推断出来。比如我们能根据 return 语句推断函数返回的类型,如下代码所示:

{/** 根据参数的类型,推断出返回值的类型也是 number */function add1(a: number, b: number) {return a + b;}const x1= add1(1, 1); // 推断出 x1 的类型也是 number/** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */function add2(a: number, b = 1) {return a + b;}const x2 = add2(1);const x3 = add2(1, '1'); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number | undefined
}

在上述 add1 函数中,我们 return 了变量 a + b 的结果,因为 a 和 b 的类型为 number,所以函数返回类型被推断为 number。

当然,拥有默认值的函数参数的类型也能被推断出来。比如上述 add2 函数中,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 类型是 numberconst x2 = add(1, '1');  // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number
}

这里我们定义了一个实现加法功能的函数类型 Adder(定义的 Adder 类型使用了 type 类型别名,声明了add变量的类型为 Adder 并赋值一个匿名箭头函数,箭头函数参数 a 和 b (也可以叫 c 和 d )的类型和返回类型都没有显式声明。

TypeScript 通过add的类型 Adder 反向(通过变量类型推断出值的相关类型)推断出箭头函数参数及返回值的类型,也就是说函数参数 a、b,以及返回类型在这个变量的声明上下文中被确定了。

正是得益于 TypeScript 这种类型推导机制和能力,使得我们无须显式声明,即可直接通过上下文环境推断出变量的类型,也就是说此时类型可缺省。

下面回头看最前面的示例(如下所示),我们发现这些缺省类型注解的变量还可以通过类型推断出类型。

{let str = 'this is string'; // str: stringlet num = 1; // num: numberlet bool = true; // bool: boolean
}
{const str = 'this is string'; // str: 'this is string'const num = 1; // num: 1const bool = true; // bool: true
}

如上述代码中注释说明,通过 let 和 const 定义的赋予了相同值的变量,其推断出来的类型不一样。比如同样是 'this is string'(这里表示一个字符串值),通过 let 定义的变量类型是 string,而通过 const 定义的变量类型是 'this is string'(这里表示一个字符串字面量类型)。这里我们可以通过 VS Code hover 示例中的变量查看类型,验证一下这个结论。

🍍字面量类型

在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。

目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:

{let specifiedStr: 'this is string' = 'this is string';let specifiedNum: 1 = 1;let specifiedBoolean: true = true;
}

字面量类型是集合类型的子类型,它是集合类型的一种更具体的表达。比如 'this is string' (这里表示一个字符串字面量类型)类型是 string 类型(确切地说是 string 类型的子类型),而 string 类型不一定是 'this is string'(这里表示一个字符串字面量类型)类型,如下具体示例:

{let specifiedStr: 'this is string' = 'this is string';let str: string = 'any string';specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'str = specifiedStr; // ok 
}

这里,我们通过一个更通俗的说法来理解字面量类型和所属集合类型的关系。

比如说我们用“马”比喻 string 类型,即“黑马”代指 'this is string' 类型,“黑马”肯定是“马”,但“马”不一定是“黑马”,它可能还是“白马”“灰马”。因此,'this is string' 字面量类型可以给 string 类型赋值,但是 string 类型不能给 'this is string' 字面量类型赋值,这个比喻同样适合于形容数字、布尔等其他字面量和它们父类的关系。

接下来,我们介绍一下字符串字面量类型、数字字面量类型、布尔字面量类型。

字符串字面量类型

一般来说,我们可以使用一个字符串字面量类型作为变量的类型,如下代码所示:

let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'

实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合。

如下代码所示,我们使用字面量联合类型描述了一个明确、可 'up' 可 'down' 的集合,这样就能清楚地知道需要的数据结构了。

type Direction = 'up' | 'down';
function move(dir: Direction) {// ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'

通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。

因此,相较于使用 string 类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。

数字字面量类型及布尔字面量类型

数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型 Config:

interface Config {size: 'small' | 'big';isEnable:  true | false;margin: 0 | 2 | 4;
}

在上述代码中,我们限定了 size 属性为字符串字面量类型 'small' | 'big',isEnable 属性为布尔字面量类型 true | false(布尔字面量只包含 true 和 false,true | false 的组合跟直接使用 boolean 没有区别),margin 属性为数字字面量类型 0 | 2 | 4。

介绍完三种字面量类型后,我们再来看看通过 let 和 const 定义的变量的值相同,而变量类型不一致的具体原因。

我们先来看一个 const 示例,如下代码所示:

{const str = 'this is string'; // str: 'this is string'const num = 1; // num: 1const bool = true; // bool: true
}

在上述代码中,我们将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定,这也是一种比较合理的设计。

接下来我们看看如下所示的 let 示例,此时理解起来可能会稍微难一些。

{let str = 'this is string'; // str: stringlet num = 1; // num: numberlet bool = true; // bool: boolean
}

在上述代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如 str 的类型是 'this is string' 类型(这里表示一个字符串字面量类型)的父类型 string,num 的类型是 1 类型的父类型 number。

这种设计符合编程预期,意味着我们可以分别赋予 str 和 num 任意值(只要类型是 string 和 number 的子集的变量):

str = 'any string';
num = 2;
bool = false;

我们将 TypeScript 的字面量子类型转换为父类型的这种设计称之为 "literal widening",也就是字面量类型的拓宽,比如上面示例中提到的字符串字面量类型转换成 string 类型,下面我们着重介绍一下。

Literal Widening

所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。

下面我们通过字符串字面量的示例来理解一下字面量类型拓宽:

{let str = 'this is string'; // 类型是 stringlet strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;const specifiedStr = 'this is string'; // 类型是 'this is string'let str2 = specifiedStr; // 类型是 'string'let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;
}

因为第 2~3 行满足了 let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为 string(形参类型确切地讲是 string | undefined)。

因为第 5 行的常量不可变更,类型没有拓宽,所以 specifiedStr 的类型是 'this is string' 字面量类型。

第 7~8 行,因为赋予的值 specifiedStr 的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2 的类型被推断为 'this is string',它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。

基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为。

{const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
}

实际上,除了字面量类型拓宽之外,TypeScript 对某些特定类型值也有类似 "Type Widening" (类型拓宽)的设计,下面我们具体来了解一下。

Type Widening

比如对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:

{let x = null; // 类型拓宽成 anylet y = undefined; // 类型拓宽成 any/** -----分界线------- */const z = null; // 类型是 null/** -----分界线------- */let anyFun = (param = null) => param; // 形参类型是 nulllet z2 = z; // 类型是 nulllet x2 = x; // 类型是 nulllet y2 = y; // 类型是 undefined
}

注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成“any”。因此,某些过时的资料中会存在差异。

在现代 TypeScript 中,以上示例的第 2~3 行的类型拓宽更符合实际编程习惯,我们可以赋予任何其他类型的值给具有 null 或 undefined 初始值的变量 x 和 y。

示例第 7~10 行的类型推断行为因为开启了 strictNullChecks=true(说明:本课程所有示例都基于严格模式编写),此时我们可以从类型安全的角度试着思考一下:这几行代码中出现的变量、形参的类型为什么是 null 或 undefined,而不是 any?因为前者可以让我们更谨慎对待这些变量、形参,而后者不能。

既然有类型拓宽,自然也会有类型缩小,下面我们简单介绍一下 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;};
}

在 VS Code 中 hover 到第 4 行的 anything 变量提示类型是 string,到第 6 行则提示类型是 number。

同样,我们可以使用类型守卫将联合类型缩小到明确的子类型,具体示例如下:

{let func = (anything: string | number) => {if (typeof anything === 'string') {return anything; // 类型是 string } else {return anything; // 类型是 number}};
}

当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:

{type Goods = 'pen' | 'pencil' | 'ruler';const getPenCost = (item: 'pen') => 2;const getPencilCost = (item: 'pencil') => 4;const getRulerCost = (item: 'ruler') => 6;const getCost = (item: Goods) => {if (item === 'pen') {return getPenCost(item); // item => 'pen'} else if (item === 'pencil') {return getPencilCost(item); // item => 'pencil'} else {return getRulerCost(item); // item => 'ruler'}}console.log(getCost('pen'), 'pen');console.log(getCost('pencil'), 'pencil');console.log(getCost('ruler'), 'ruler');
}

打印结果

在上述 getCost 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if 语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。

那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenCost、getPencilCost、getRulerCost 呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === 'pencil' 的分支,item 的类型就被收缩为“pencil”。

事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:

const getCost = (item: Goods) =>  {if (item === 'pen') {item; // item => 'pen'} else {item; // => 'pencil' | 'ruler'}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/220733.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谈谈数据归一化与标准化

背景: 归一化(Normalization)和标准化(Standardization)是常用的数据预处理技术,用于将不同范围或不同单位的特征值转换为统一的尺度,以便更好地进行数据分析和模型训练。一句话:消…

Go EASY游戏框架 之 RPC Guide 03

1 Overview easy解决服务端通信问题,同样使用了RPC技术。easy使用的ETCDGRPC,直接将它们打包组合在了一起。随着服务发现的成熟,稳定,简单,若是不用,甚至你也并不需要RPC来分解你的架构。 GRPC 有默认res…

银河麒麟重置密码

桌面版银河麒麟重置密码 1.选择界面按e 出现银河麒麟系统选择的页面,我们点击键盘上的“e”键,进入电脑启动项编辑页 2.编辑启动页 在启动项编辑页面,我们将光标移动到linux这一行的最后,然后输入“init/bin/bash consoletty0”…

给一个容器添加el-popover/el-tooltip内容提示框

效果&#xff1a; html: <div class"evaluate"><div class"list flex-column-center" v-for"(item, index) in evaluateList" :key"index"mouseenter"mouseenterHandler(item)" mouseleave"mouseleaveHandle…

【Vue第5章】vuex_Vue2

目录 5.1 理解vuex 5.1.1 vuex是什么 5.1.2 什么时候使用vuex 5.1.3 案例 5.1.4 vuex工作原理图 5.2 vuex核心概念和API 5.2.1 state 5.2.2 actions 5.2.3 mutations 5.2.4 getters 5.2.5 modules 5.3 笔记与代码 5.3.1 笔记 5.3.2 23_src_求和案例_纯vue版 5.3…

什么是跨站脚本攻击(XSS)?如何防止它?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

【面试】数据库—优化—聚簇索引和非聚簇索引、回表查询

数据库—优化—聚簇索引和非聚簇索引、回表查询 1. 什么是聚簇索引什么是非聚簇索引 ? 聚集索引选取规则: 如果存在主键&#xff0c;主键索引就是聚集索引&#xff1b;如果不存在主键&#xff0c;将使用第一个唯一&#xff08;UNIQUE&#xff09;索引作为聚集索引&#xff1b…

【移动通讯】【MIMO】[P1]【科普篇】

前言&#xff1a; 前面几个月把CA 的技术总体复盘了一下,下面一段时间 主要结合各国一些MIMO 技术的文档,复盘一下MIMO. 这篇主要参考华为&#xff1a; info.support.huawei.com MIMO 技术使用多天线发送和接受信号。主要应用在WIFI 手机通讯等领域. 这种技术提高了系统容量&…

MySQL和Redis有什么区别?

目录 一、什么是MySQL 二、什么是Redis 三、MySQL和Redis的区别 一、什么是MySQL MySQL是一种开源的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它是最流行的数据库之一。MySQL以其高性能、可靠性和易用性而闻名&#xff0c;广泛应用于各种Web应用程序…

ACM-MM2023 DITN详解:一个部署友好的超分Transformer

目录 1. Introduction2. Method2.1. Overview2.2. UFONE2.3 真实场景下的部署优化 3. 结果 Paper: Unfolding Once is Enough: A Deployment-Friendly Transformer Unit for Super-Resolution Code: https://github.com/yongliuy/DITN 1. Introduction CNN做超分的缺点 由于卷…

Leetcode—709.转换成小写字母【简单】

2023每日刷题&#xff08;五十八&#xff09; Leetcode—709.转换成小写字母 实现代码 char* toLowerCase(char* s) {int len strlen(s);for(int i 0; i < len; i) {if(s[i] > A && s[i] < Z) {s[i] tolower(s[i]);}}return s; }运行结果 之后我会持续更…

java全栈体系结构-架构师之路(持续更新中)

Java 全栈体系结构 数据结构与算法实战&#xff08;已更&#xff09;微服务解决方案数据结构模型(openresty/tengine)实战高并发JVM虚拟机实战性能调优并发编程实战微服务框架源码解读集合框架源码解读分布式架构解决方案分布式消息中间件原理设计模式JavaWebJavaSE新零售电商项…

(04730)半导体器件之晶体三极管

晶体三极管的结构和分类 晶体三极管具有三个区、两个PN结&#xff0c;从三个区分别引出三个电极而构成&#xff0c;其结构和符号如图2.1.13所示。 晶体三极管内部的三个区&#xff0c;分别称为发射区、基区和集电区&#xff0c;其中基区十分薄&#xff0c;一般为1um至几十um,掺…

单日30PB量级!火山引擎ByteHouse云原生的数据导入这么做

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 近期&#xff0c;火山引擎ByteHouse技术专家受邀参加DataFunCon2023&#xff08;深圳站&#xff09;活动&#xff0c;并以“火山引擎ByteHouse基于云原生架构的实时…

学习笔记 -- TVS管选型参考

一、TVS管基本工作原理 当TVS管(瞬态电压抑制器)两极受到反向瞬态高能量冲击时&#xff0c;能以纳秒(ns)量级的速度&#xff0c;将两极间的高阻抗变为低阻抗&#xff0c;使两极间的电压箝位于一个预定的值&#xff0c;有效地保护电子线路中的元器件。 在浪涌电压作用下&#xf…

ETLCloud详解,如何实现最佳实践及问题排查

ETLCloud介绍 ETLCloud是新一代全域数据集成平台&#xff0c;领先于市场同类产品的数据集成平台(DataOps)&#xff0c;只需单击几下即可完成数据清洗转换、传输入仓等操作&#xff0c;具备高效、智能、一站式的全域数据集成优势&#xff0c;如&#xff1a; 毫秒级实时数据同步 …

UE虚幻引擎中程序无需运行也可调试

首先先新建一个蓝图类&#xff0c;在蓝图类中创建一个Custom event 事件&#xff0c;然后在右侧细节面板中搜索call in editor&#xff0c;编译保存之后&#xff0c;将该蓝图类拖拽到关卡场景中&#xff0c;在细节面板中即可看到该事件的按钮。

车载导航系统UI界面,可视化大屏设计(PS源文件)

大屏组件可以让UI设计师的工作更加便捷&#xff0c;使其更高效快速的完成设计任务。现分享车载导航系统科技风蓝黑简约UI界面、车载系统UI主界面、车载系统科技风UI界面、首页车载系统科技感界面界面的大屏Photoshop源文件&#xff0c;开箱即用&#xff01; 若需 更多行业 相关…

vxe-table循环生成表格,表格里的某些数值设置颜色

业务需求&#xff1a;表格的列名是循环出来的&#xff0c;后台返回每行的表格数据结构如下&#xff0c;需要表格里只有“当期”的行里数值超限waterGuildLine后显示红色。需要在vxe-table里写个插槽&#xff0c;再写个方法。 <vxe-table border ref"dayTableRef" …

【MySQL】——数据类型及字符集

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…