侧边栏壁纸
博主头像
前端自习室博主等级

折腾是进步的阶梯

  • 累计撰写 30 篇文章
  • 累计创建 0 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

typescript入门笔记(一):初见类型

lumozx
2022-11-06 / 0 评论 / 0 点赞 / 39 阅读 / 48245 字

注:本文typescript版本为5.0.2

我们都知道,typescript 是微软开发和维护的一种面向对象的编程语言。它是 javascript 的超集,包含了 javascript 的所有元素,可以载入 javascript 代码运行,并扩展了 javascript 的语法。但 typescript 并非是一个没有质疑的版本。它带来代码的健壮性的同时,限制了 javascript 的灵活性,同时 typescript 并不会提高我们项目的性能,却需要我们开发额外的代码。

但这意味着弊大于利吗?

答案是否定的,我们提到,typescript 是 javascript 的超集,因此,在 es 中一些没有落地的提案,会在 typescript 中早早进行实现(比如装饰器),因此我们可以使用更加便利的语法和特性,来提高我们的开发效率,另一方面,在开发轻量级的项目的时候,javascript 的确是一个看起来不错的选择,但当项目逐渐庞大、代码的大小、复杂性和出错率增加的时候,javascript 的灵活反而是是一种隐患,我们无法确定这些变量是什么,怎么用。换句话说:我们不知道这些变量是什么类型!

因此我们回头看看 typescriptjavascript 的区别,我们发现,牺牲 javascript 的灵活性从而换来代码的健壮性正是我们想要的结果,对于其他非类型的灵活性:比如原型链、闭包等,typescript 并没有进行任何限制。试想:当你对 javascript 项目某个文件进行拆分、或者重构的时候,针对不同变量,我们需要在在控制台打印出来,确认他们的值或者返回值是符合期望的。但在 typescript 项目中,我们在编写阶段,就有类型自动推导,而不用实际运行。

因此,学习 typescript 并非弊大于利,而是利远远大于弊,因此 typescript 是一个值得我们学习的js超集,但真正上手的时候,往往会出现浅尝辄止的场景,这主要是较小的项目并不需要 typescript ,同时,较为陡峭的学习难度,也是阻挡深入学习的大山,增加 typescript 学习难度主要原因有两个:

  1. javascript 的习惯惯性

  2. 缺少对 typescript 的深入思考。

本质上,typescript 由三个部分组成:类型、语法与工程。

类型是 typescript 的核心,也是学习成本最高、最为陡峭的部分,除了我们可以对 javascript 进行类型标注以外,它还内置了一批类型工具,来辅助我们完成一系列类型"体操"(包括泛型),而这些"体操",就是 typescript 的精髓所在,因此我们需要深入了解、学习这些体操,而非简单来认识一下类型。

语法我们在上面提到过,我们可以在 typescript 中使用一些已经在Stage 3 / 4 阶段、或者比较重要的提案,同时我们可以在tsconfig中进行对应配置,来开启、关闭使用这些功能的能力,或者实现语法降级,来让代码在低级浏览器中运行。

工程就是我们的协作能力,也就是tsconfig的配置或者d.ts以及eslint和类型声明、命名空间等有关整个工程描述。

我们提到过,typescriptjavascript 的超集,那么意味着,他本身是包含 javascript 的相关逻辑的,那么在类型方面,typescript javascript 进行了什么改造呢?我们都知道,javascript 的类型包括原始类型和对象组成。我们先回顾一下 javascript 有什么原始类型:他包括number、string、boolean 、null 、undefiend 、symbol 、bigint,因此,在 typescript 中,他们的类型就对应着他们的类型注解,除了null undefiend

null、undefined、void

javascript 中,null 和 undefined 的含义是不同的,在 javascript 中,未经定义的变量都是 undefined 表示的是这里没有任何值,而 null 是需要经过赋值操作而得来,表示的这里有值但为空。在 typescript 中,他们都是有具体含义的类型,也就是说,他们都是表示当前的变量,是存在的与当前的逻辑系统中的,是可控可预料的(在 javascript 出现 undefined 是不可控的表现)。同时,在未开启严格检查的情况下(关闭strictNullChecks),他们都是其他类型的子类型,也就是说,他们所对应的变量,是可以赋值给其他类型的变量的,比如

一般情况下,我们都会开启 strictNullChecks ,来严格检查我们的代码,从而避免丢失一些健壮性。

除了null和undefined,我们还经常见到void,但这并不经常在项目中遇到他,在 javascript 中,void 操作符后面会跟着一个表达式,并返回 undefined,但在 typescript 中,viod是一个源氏类型标注,所表示的含义是描述一个内部没有 return 语句,或者return了,但后面没有return任何东西的情况。这里需要明确一点是,return undefined 和 return 所代表的的类型是不同的,即使他们的实际值都是undefined,但前者的类型是undefined,后者是void。

当然在未开启 strictNullChecks情况下,undefined 和 null 也是void的子类型,他们都可以被赋值给void。

数组、元组

本身数组并非 javascript 的类型,在 javascript 中,他们都是对象,对在 typescript 中是一个常用的类型,他有有以下声明方式

const arr1: string[] = [];
const arr2: Array<string> = [];   

而使用方式就是正常的使用方式,如果数组中的值并不是单一类型的话,我们可以使用联合类型,这个之后会讲到。

但如果我们除了对类型进行约束外,还想约束数组的长度,那么此时,我们可以用约束性质更强的元组类型。

元组是表示一个已知元素数量、类型的数组,各个元素的类型不必相同,比如

const arr: [string,number] = ['hello', 11]

这样,我们就可以精准的将数组的每个元素定义上类型注解,然后将数组长度进行限制。当我们的取值超过元组长度的时候,比如arr[10],就会报越界访问的错误。

但是,过于死板的约束,导致元素被钉死在元组类型上,也就是说,某一元素位置的缺失,就会让当前值变成一个截然不同的类型,即使在使用的时候,这个元素的缺失是可接受的。

那么我们就可以使用可变元组。

const arr: [string, number?, boolean?] = ['hello'];

这样,我们的约束就没有那么死了,空下的值类型就是undefined,但这里需要注意,number? 并不等于 number | undefined,在 typescript 中 undefined也是一个有值的类型。一起改变的还有元组长度类型,从之前的长度类型为3,变成类型1 | 2 | 3。

那么,现在元组就是一个没有明显缺点的趁手工具了吗?

显然不是。

注意上面的元组,我们只知道在当前位置是什么类型,却不知道这个类型的含义,比如我这里有一个描述一个长方体数据的元组,那么他的长宽高作为元组的第1、2、3项

const cuboid: [number, number, number] = [1,2,3]

但当我们使用的时候,我们并不知道,这三个数据是长宽高,他们有可能是高宽长,因此,后来 typescript 引入了具名元组,也就是说可以像定义接口一样,定义元组。当然,也支持可选元素。

const cuboid: [long:number, wide:number, high:number, volume?:number] = [1,2,3]

到目前为止,看起来元组的功能慢慢齐全了,由于在类型层面限制了数组的长度,因此元组可以在任何越界访问的情况下弹出警告,由此可以进一步提升数组结构的严谨性。

对象

万物皆对象,对象的数据结构是多种多样的,在 typescript 中,我们通过接口,也就是 interface 来描述对象。我们思考一下,如何描述一个对象呢?

  1. 首先是对象的值的类型,这个值可以是可选的

  2. 不能缺少必要的值,或者不能随意增加非可选的值

  3. 这个对象的值可以是只读的

我们来看第一条,对象的值有类型,同时这个值是可选的,那么按照元组的模式,我们很容易写出一个例子

其中func是可选类型,因此cuboid是一个合法的对象,在元组那里我们讲过,可选类型会在原类型的基础上增加undefined类型,但并不代表他可以直接增加undefined联合类型,而不用使用可选符号。

第二条,不能缺少必要的值,也不能随意增加非可选的值,也就是说,我们在定义对象后,手动增加func函数是合法的,但是在定义的时候,缺少name或者valume,将会报类型错误,同理,如果使用delete删除他们,也是会报错,但是删除func不会,因为func是可选的

第三条,对象的值是只读,设置只读的目睹就是为了防止被修改。

我们在具名元组接触到类似对象的写法,但我们没法对某一个元素设定只读,而是对整个数组或者元组设置为只读,一旦设置为只读,那么当前数组就不再有push、pop等方法,类型也从Array变成ReadonlyArray。

但是对象是可以给某一个元素设置只读的。

除了 interface ,还可以用type来描述对象,这两个的实际效果是类似的,大部分场景中的 interface 可以被type 来代替,不过按照使用场景来说,type是类型别名,而 interface 是更加底层的数据描述接口,因此就个人而言,type 是 interface 的上级类型,或者是所有基本类型的上级使用方法。同时 interface 只能作为联合类型的子类型,type却可以声明联合类型。

说到了对象,那么就不得不说一下装箱类型。

让我们回顾一下原型链的知识,原型链的顶端是Object和Function,或者说是Object.prototype,作为原型链的定点,由于__proto__指向自身,从而赋值为null。

也就是说Object是包容万物的,而在 typescript 中,Object类型也是包容任何类型的,在非严格模式下,任何类型都将赋值给Object类型。

同理。还有 Boolean、Number、String、Symbol,他们都包含着反直觉的类型,比如包含 null 、 undefined 、 void不能说不合理,因为细细一想,在 javascript 中,他们真的是可以是这些值。这就是装箱类型,在任何之后,都不建议使用这些类型,因为他们的类型是不可控的。

object的引入,就是为了解决Object装箱类型的错误使用,代表所有非原始类型的类型,也就是上文说的:对象——数组、函数、对象。

而其他的拆箱类型,比如string、number等,就代表着他们本身的原始类型。

同时,虽然我们可以使用 [] 来表示空数组,但我们不能使用 {} 来表示空对象,因为对象与数组不同,非具名定义的对象类型,它的类型覆盖范围极大,即使是object也包含着所有的非原始类型,而 {} 所代表的含义就是内部无属性定义的空对象,也就是说,{} 就是没有任何属性的Object,可以标识一切,但不能操作一切。因此,我们需要如同避免使用any一样,避免使用 {} 。

原始类型基本介绍完了,但似乎还漏下一个类型,那就是symbol,我们知道拆箱类型基本是可以代表他们自己的类型,但是symbol呢?symbol在 javascript 中是表示【不同值】这一特性,但是他们都具有相同的symbol类型,这一点却与【不同值】这一特性背道而驰,为了在类型上实现这个特性,可以采用unique symbol这个类型实名,他是symbol 类型的子类型,但每个unique symbol 类型都是独一无二的

枚举

对于 javascript 来说,枚举是一个新东西,即使是在其他语言里面经常出现,但关于枚举的提案依然并没有被提交,不过没关系, typescript 已经实现了枚举的概念,在 typescript 只中,枚举了类似我们在日事清中的R_URL的constants,类似以对象的形式,比如

enum base { 
  AUTH_ACTION = "/login/authAjax", 
  LOGOUT = "/logout/index", 
  LOGOUT_AJAX = "/logout/index2" 
}

也就是说,我们把一下常量,放在一个命名空间中,如果,在枚举中并没有声明对应的值,那么就会使用数字枚举,也就是说,会自动将第一个值置为 0 ,然后以1进行递增。

我们也可以对常量指定起始值,那么之后的数字枚举,将会在当前常量的基础上进行递增,不仅如此,我们还可以使用延迟求值,将某一值设置为函数返回值,但这会产生一个后果:之后的值需要手动初始化,也就是说 typescript 的编译器并不会在编译过程处理自增的逻辑中执行js函数。

枚举和对象还有一个差异,那就是对象的映射是单项的,也就是说,我们只能从对象的key取到value,并不能从value取到对象的key,而枚举却可以从value,取到key。当然,这并不意味着枚举没有对象映射的问题:key是唯一的,但value并不是。当我们使用枚举的value来获取key的时候,我们获取的key是最后一个匹配的key。比如

在上面的例子,这个枚举有三个不同的key,A,B,C,他们的value都是0,那么当我通过0获取key的时候,获取的是C,也就是最后一个匹配的key,通过阅读编译后的js,我们可以知道产生这个行为的原因:

枚举编译成 javascript ,其实就是一个双重赋值的性质,之所以最后一个匹配的key,那是因为后一个相同value的值,覆盖了前一个。

那么,枚举的类型应该如何表示呢?

在官网文档中,提出了一个概念:字面量枚举成员,指的是不带初始值的常量枚举成员,或者被初始化为:

  • 任何字符字面量,比如'foo','bar','baz'

  • 任何数字字面量,比如 -1, -100, 1,100

我们注意到,延迟求值并不在这里面。只要当一个枚举成员都用于字面量枚举值的时候,这个枚举可以被当做一个类型。

同时,这个类型中针对字符字面量和数字字面量,有着不同的表现,让我们来看一组例子。

我们来总结一下:当我们使用枚举作为类型的时候

  1. 如果是字符字面量的类型,那么只有相同枚举中,相同的value的类型可以相互同通用

  2. 如果是数字字面量的类型,那么只有相同枚举中,相同的value的类型可以相互同通用,来自常量的任何数字,也可以对此类型进行赋值。

字面量类型、联合类型、交叉类型

既然我们提到了字面量,那么我们就顺势提一下字面量类型,经过上文的枚举,我们意识到:并不是只有 javascript 的原始类型能当做类型,字面量也是可以当做类型的,比如当前有个接口,是以下的形式

那么 code 的值,只能选择200 、 400 、500,status只能使用 success 和 fail 两个值,这样比原始类型更加精确,让我们的值在原始类型至上,进一步限制,防止出现意料之外的相同原始类型的值。

除了字面量类型外,还可以使用字符串模板类型

可以通过模板字符串,来定制字面量类型的灵活性,这种类型叫做字符包模板类型

不过,很少见单独使用字面量类型,而是在使用联合类型的时候,组合使用。

联合类型可以理解为一组类型可用的集合,只要符合这些类型集合中的任意一个,就说明当前值是符合当前类型的,通常用 | 来分割不同的类型。比如上文的code

这个听起来很简单,但需要有几点

  1. 如果是函数类型,需要用()进行包裹

  2. 联合类型是可以在类型中进行嵌套的,但最后都会进行平铺到一级上去。

既然有联合类型的 | ,那么就会有交叉类型 & 。

交叉类型统一也是合集,使用 & 连接,但此时交叉类型会合并成一个类型,要求当前的值符合当前所有类型,不过,交叉类型使用场景少于联合类型,因为类型中的接口子类型是不可控的,如果存在同一字段,不同类型,那么会被合并为never类型。意味着【不可能存在的类型】。意味着异常。

函数

我们在上面提到了函数也是有类型的,那么我们如何定义函数的类型呢?

函数类型

函数需要类型的地方,第一个是入参的类型,第二个是返回值的类型,他们相互组合,就变成了函数的类型。

我们来回忆下,在babel那一章,我们如何定义函数的

第一种,通过使用function函数声明的方式,可以在参数与函数体之前编写类型注解

第二种,如果使用函数表达式的方式来声明一个函数,除了可以使用function函数声明的方式来添加类型注解外,还可以使用类似箭头函数的写法,来定义函数的类型

第三种,我们可以使用箭头函数来定义函数,类型注解与第一种和第二种类似。

我们还可以用type 将函数类型抽取出来,来让这个类型成为一个公共类型

其实,我们我不光可以使用函数类型来描述函数,用接口同样可以来描述函数,这种使用方法,让我们更加偏向函数的结构

interface Foo {
   (name: string): number 
}

看起来很简单,但我们知道,函数还有可选参数和rest参数以及默认参数,可以让我们的参数更加灵活,但在定义的类型上,应该怎么表示呢?

在之前我们提到过,可选类型可以使用问号来表示,那么在函数中,我们也可以用问号来表示可选参数,同时,在某些情况下,我们可以使用rest参数,来处理所有可能传进来的参数,因此,我们只要在rest参数后面,标明他的数组类型就可以。

this

javascript 中,this的使用是一场巧妙地旅行:你永远需要探索this到底指向哪里,而在 typescript 中,我们可以自定义this的类型,以保证我们的函数是在合理的范围之中使用的,关于 javascript 的this使用这里就不展开介绍了,我们假设你已经了解了常见的this使用场景。

我们可以在函数的第一个参数,来定义this的类型

当我们将this的类型指向Obj的时候,我们调用Obj的fn方法,此时fn方法的this是指向obj的,也就是Obj类型,因此是合法的,但一旦我们使用apply指向非Objl类型的时候,就会抛出错误。

重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。本身 javascript 是没有这个概念的,同时 typescript 的重载并非要严格意义上的重载,而是函数类型的重载,具体实现,依然需要内部来判断类型,进行对应的处理。

假设我们有一个函数,当入参是数字类型的时候,返回字符串,当入参是字符串的时候,返回数字类型,我们使用 typescript 来处理一下这个函数

可以看到,我们在这里写了三个函数声明

  • 第一个,当foo是number的时候,返回字符串

  • 第二个,当foo是字符串的时候,返回数字

  • 第三个,包含所有入参的类型,返回所有可能出现的类型

基于重载签名,我们实现了入参和返回值相关联的情况,让我们的类型标注更加精确,这里有个问题,那就是在第三个函数声明的时候,我们实际的声明是运行入参是字符串,返回值是字符串的,因此这个重载逻辑需要泛型以及类型工具进行优化,这个是之后的内容。

如同if else一样,重载的匹配也是从上向下进行查找的,因此顺序也是影响匹配的规则之一。

class

我们来到了最后一个基础难点——类,原本 javascript 也是没有类这个概念的,在es6中,增加了类的语法糖,为什么说是语法糖呢,因为在实际上,依然是基于原型链的操作,只是写法上,跟其他编程语言的类比起来是相似的。

假设我们已经了解了 javascript 类的基本知识,那么我们放在 typescript 中,如何进行标注类型呢?

在 constructor 和 set 中,返回值是不用并且不可被标注的,因为他们的返回值是没有意义的。

修饰符

我们可以在 typescript 中使用修饰符,是的,虽然 javascript 已经可以使用类的语法,但是却不支持修饰符,在 typescript 中,修饰符分为访问性修饰符,他们包括publicprivateprotected 。还有一个readonly,属于操作性修饰符。

在类中,默认的修饰符是 public ,意味着我们可以自由访问被 public 标注的成员,包括在类、类的实例、甚至子类,都可以被访问到。

当成员被标记是 private 的时候,他不能在声明他的类外部进行访问,也就是说仅仅在当前类的逻辑里面的变量。

而对于 protected ,他的行为和 private 相似,但有一点不同,那就是可以在子类进行访问,当然实例和子类的实力依然是不可直接访问的。

readonly 与接口的 readonly 意义一致,不参与类型兼容的逻辑,但这里需要注意,在构造函数中是可以进行对 readonly 成员赋值的,即使 readonly 成员存在初始化值。

我们可以在构造函数中的参数上,使用修饰符,这样我们可以集中定义、初始化成员。

我们已经简单了解修饰符了,那我们发散一下,如果将一个类作为一个类型,成员和修饰符之间的区别,会影响类型的兼容吗?我们来实验一下,现在有以下代码:

我们看到,当成员相同,修饰符都是 public 的情况下,他们的类型是兼容的,可以相互赋值。

但如果修饰符进行一下改动呢?

从这个例子可以看出来,即使是修饰符相同,但是非 public 的情况下,只有子类的类型是可以兼容的,非子类的情况下,即使是结构一样,也是不兼容的,同样的 protected 也存在相同的逻辑。

静态成员

我们可以使用 static 关键字,来表示一个成员是静态成员,与需要修饰符访问的成员不同,静态成员无法通过this来访问,而是通过直接在访问类的属性、方法来获取。而实现这个逻辑的关键点,就是静态成员被挂载到当前的函数体上,而非成员被挂载到原型链上。因此静态成员不会被this访问到,也不会被实例继承,他仅仅是属于当前被定义的类。

静态成员之前可以增加修饰符,因此对于这些静态成员,也可以使用修饰符所代表的的逻辑。

继承

我们在前面也提到过子类,实际上就是依靠继承来实现的子类,严谨一点,父类被称为基类,子类被称为派生类,父类通过修饰符,来确定子类可以访问父类的哪些属性,同时,子类也可以覆盖父类的方法,也可以直接通过 super 调用父类的方法。

不过,看到这里,大家可能有疑问了,既然子类可以覆盖的方法,也可以调用父类的方法,但这样是不是太简单粗暴了,如果子类想要覆盖父类的某一个方法,但实际父类并没有此方法,那么子类就以外增加了一个全新的方法!

针对这个,typescript 提供了 override ,表示着尝试覆盖基类的方法,如果父类不存在对应的方法,那么不会通过类型检查。

抽象

抽象类同样是一个比较重要的概念,在概念广泛用于JAVA 等其他语言,意味着,是对类和方法的抽象,也就是说,抽象类描述了一个类应该有哪些成员、一个抽象方法描述了,这个方法在的入参类型和返回值类型。

不管是类还是成员,抽象用 abstract 关键字声明,没有此关键字的成员,不会作为抽象成员,而是作为正常的父类成员,不过,抽象类尽可能的不要声明静态成员的,因为按照逻辑,抽象类抽象的,是不存在的,既然不存在,那么就不存在可以直接访问的静态成员,尽管语法支持,但逻辑上是走不通的。

现在回顾一下访问性修饰符,他们包括publicprivateprotected

但是这三种状态作用到构造函数上,会变成什么样的呢?

首先默认是 public ,因此类的行为并没有任何影响。

如果是 private 的话,我们会发现,类并不允许实例化了,一个并不允许实例化的类听起来似乎没有用处,但如果他的成员都是静态成员的时候,我们可以用这个类来管理这些静态成员,毕竟全是静态成员的类,确实没有必要也不太合逻辑去实例化。

如果是 protected 的话,联想到 protected 的特性,我们无法直接实例父类,而是需要子类继承,然后实例子类,这一步操作让我们想到了抽象类的特性,因此我们可以通过这种操作,来实现一些具有抽象类特性的类。

实现与接口继承

我们看到抽象类,他们有没有很像之前的接口,实际上,在 typescript 中,接口也会跟类进行交互的,接口定义了一些数据,我们可以把他看做类的描述,而类根据这个结构,形成对应功能的类,那这个过程就叫做实现。

实现使用关键字 implements ,我们来看一下怎么使用

同时,一个类可以实现多个接口,而实现的过程类似交叉类型,以逗号分隔,相同成员保持类型一致,否则依然会报错。

看到这里,可能大家有些迷糊,抽象类可以让类继承,而接口也可以让类进行实现,那么接口和抽象类有什么区别呢?

首先,抽象类是用来规定子类的通用特性的,而接口,是成员的集合,抽象类不能实例化,是一种用来创建子类模板,但是接口是一种形式,接口内部不能处理任何逻辑。

其次,抽象类的方法是可以有自己默认实现的,但接口是完全抽象,不存在默认实现。同时,重写父类是需要 override 关键字,但实现接口的方法却不能使用此关键字。

除了这些操作,还有接口还可以继承类,在官方文档中提到,在声明一个类的时候,同时也声明了一个类的实例的类型。

因此,与其说接口继承是类,倒不如说接口继承的是表示这个类的结构的类型,因此,是可以直接在写法使用接口继承类的。

内置类型

我们在之前提到过never类型,其实,在 typescript 中,还提供了其他内置类型,来让我们获得更好的体验。

我们来回忆一下,是不是经常遇到一种情况:我们把一些变量或者常量放入console.log中,会发现他都可以进行打印处理,那么console.log的入参类型是什么呢?首先需要排出的是联合类型,因为还有用户自己定义的类型别名等,联合类型显然不太实际,为了实现这个逻辑, typescript 提给了 any 类型,来表示任意类型。

一个被标记为 any 的类型可以接受任意类型的值,也就意味着他是一个任意类型,能兼容任意类型,也能被任意类型的兼容,因此滥用 any 导致的后果是灾难的,他导致了 typescript 的类型检查形同虚设,退化成了 javascript 。因此为了减少 any 的使用,我们能不用 any 就不用 any

除了any,还有一个类似的类型: unknown 类型。一个 unknown 类型的变量可以再次被赋值为任意其它类型,但只能赋值给 anyunknown 类型的变量。

any 便不会出现这种情况,产生这种情况的原因就是 unknown 是任何类型的父类型,但 any 既是所有类型的父类型,又是所有类型的子类型。因此,当其他类型赋值给 unknown 类型的变量的时候,是可以做到兼容,但如果 unknown 赋值非 anyunknown 类型的变量的时候,就会抛出错误,因为父类型是不能赋值给子类型。因此这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用typeof、类型断言等方式来缩小未知范围

因此,在未知类型的时候,推荐语义和范围更加正确的 unknown 。不过既然包含任何类型的 unknown ,那么就存在什么都不包含的 never 类型。

说到不包含任何东西,我们很快能想到前文的 void ,但实际上,void 所代表的的不包含任何东西的类型,虽然不包含任何东西,但依然是一个类型,而 never 是连类型都是空的,他不携带任何类型信息,因此即使在联合类型中使用 never ,也会被忽略,他是所有类型的子类型,但只有 never 类型的变量才能赋值给另一个 never 变量,这里需要注意,即使是 any 也无法赋值给 never

never 类型的特性也意味着他不会出现在正常项目逻辑中,因此他的出现,都是出乎意料的、不合常规、不合逻辑的。

因此在抛出错误的语句中,我们才会使用 never 。如果抛出的错误是在一个函数中,那么函数的返回值也可以是 never ,因为抛出错误会导致函数停止执行,因此返回值就变成了死代码。

亦或者数组在初始化的时候,并没有给予类型,从而会被推导为 never[]

或者是永远无法触达的死代码,比如一个函数中,存在永远循环的while循环,那么他的返回值也会是 never ,因为死循环导致了返回值变成了死代码。

类型断言

我们在上文提到了断言,那么我们就在这稍微讲一下,断言就是显式告诉编译器:不要管你推导出来的类型,也不要管他所在的变量是什么类型,看这个断言他就是这个类型。

断言分为两种,一种是尖括号语法,另一种是as语法。

当然了,断言也可以让当前类型变成 any ,让代码再次跌入不可维护的状态,因此类型断言同 any 一样,尽可能地少用。

断言其实也不是说啥就是啥,如果断言的类型与之前差别过大,依然会被抛出类型错误。

根据提示,我们需要断言成 unknown ,然后再次断言成为我们想要的类型。

这种断言称为双重断言。由于两种类型并没有直接的从属关系,因此,需要我们来将他断言到最顶层类型,也就是 unknownany ,之后再次断言为他们的类型,从而产生了双重断言。

也就是说,首次断言并非一定是 unknown 类型,而是只要是这两个类型的共同父类型,都可以进行双重断言。

也就说,类型是有层级关系。

还有一种断言,我们在使用 typescript 的时候经常用到,那就是非空断言。

存在一个对象,他的某个方法是可选参数,虽然我们增加了必要的可选成员,但当我们调用可选成员的时候,依然会提醒当前成员可能为空。

像这种情境,我们就可以使用非空断言,来确保编译正确,除了这种情境以外,还有querySelector等dom方法,可以使用非空断言,来确定自己肯定能查找到对应的元素。

小结

我们终于接触到了 typescript,这一章是入门章节,我们为了了解之后错综复杂的类型变换,不得不补充一些知识。而这些知识只是皮毛,我们在之后还会反反复复进行深挖,不断重新回过头来回顾这一章,从而达到由浅入深的目的。了解了这一章之后,我们再认识一下typescript提供的类型工具,就可以一头钻进这些类型相互缠绕的体操中了。那时候,我们就面各种类型交至出来的迷宫。但是,这并非我们的终点,而是我们新的起点。

0
博主关闭了所有页面的评论