注:本文typescript版本为5.0.2
我们都知道,typescript 是微软开发和维护的一种面向对象的编程语言。它是 javascript 的超集,包含了 javascript 的所有元素,可以载入 javascript 代码运行,并扩展了 javascript 的语法。但 typescript 并非是一个没有质疑的版本。它带来代码的健壮性的同时,限制了 javascript 的灵活性,同时 typescript 并不会提高我们项目的性能,却需要我们开发额外的代码。
但这意味着弊大于利吗?
答案是否定的,我们提到,typescript 是 javascript 的超集,因此,在 es 中一些没有落地的提案,会在 typescript 中早早进行实现(比如装饰器),因此我们可以使用更加便利的语法和特性,来提高我们的开发效率,另一方面,在开发轻量级的项目的时候,javascript 的确是一个看起来不错的选择,但当项目逐渐庞大、代码的大小、复杂性和出错率增加的时候,javascript 的灵活反而是是一种隐患,我们无法确定这些变量是什么,怎么用。换句话说:我们不知道这些变量是什么类型!
因此我们回头看看 typescript 与 javascript 的区别,我们发现,牺牲 javascript 的灵活性从而换来代码的健壮性正是我们想要的结果,对于其他非类型的灵活性:比如原型链、闭包等,typescript 并没有进行任何限制。试想:当你对 javascript 项目某个文件进行拆分、或者重构的时候,针对不同变量,我们需要在在控制台打印出来,确认他们的值或者返回值是符合期望的。但在 typescript 项目中,我们在编写阶段,就有类型自动推导,而不用实际运行。
因此,学习 typescript 并非弊大于利,而是利远远大于弊,因此 typescript 是一个值得我们学习的js超集,但真正上手的时候,往往会出现浅尝辄止的场景,这主要是较小的项目并不需要 typescript ,同时,较为陡峭的学习难度,也是阻挡深入学习的大山,增加 typescript 学习难度主要原因有两个:
javascript 的习惯惯性
缺少对 typescript 的深入思考。
本质上,typescript 由三个部分组成:类型、语法与工程。
类型是 typescript 的核心,也是学习成本最高、最为陡峭的部分,除了我们可以对 javascript 进行类型标注以外,它还内置了一批类型工具,来辅助我们完成一系列类型"体操"(包括泛型),而这些"体操",就是 typescript 的精髓所在,因此我们需要深入了解、学习这些体操,而非简单来认识一下类型。
语法我们在上面提到过,我们可以在 typescript 中使用一些已经在Stage 3 / 4 阶段、或者比较重要的提案,同时我们可以在tsconfig中进行对应配置,来开启、关闭使用这些功能的能力,或者实现语法降级,来让代码在低级浏览器中运行。
工程就是我们的协作能力,也就是tsconfig的配置或者d.ts以及eslint和类型声明、命名空间等有关整个工程描述。
我们提到过,typescript 是 javascript 的超集,那么意味着,他本身是包含 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 来描述对象。我们思考一下,如何描述一个对象呢?
首先是对象的值的类型,这个值可以是可选的
不能缺少必要的值,或者不能随意增加非可选的值
这个对象的值可以是只读的
我们来看第一条,对象的值有类型,同时这个值是可选的,那么按照元组的模式,我们很容易写出一个例子
其中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
我们注意到,延迟求值并不在这里面。只要当一个枚举成员都用于字面量枚举值的时候,这个枚举可以被当做一个类型。
同时,这个类型中针对字符字面量和数字字面量,有着不同的表现,让我们来看一组例子。
我们来总结一下:当我们使用枚举作为类型的时候
如果是字符字面量的类型,那么只有相同枚举中,相同的value的类型可以相互同通用
如果是数字字面量的类型,那么只有相同枚举中,相同的value的类型可以相互同通用,来自常量的任何数字,也可以对此类型进行赋值。
字面量类型、联合类型、交叉类型
既然我们提到了字面量,那么我们就顺势提一下字面量类型,经过上文的枚举,我们意识到:并不是只有 javascript 的原始类型能当做类型,字面量也是可以当做类型的,比如当前有个接口,是以下的形式
那么 code 的值,只能选择200 、 400 、500,status只能使用 success 和 fail 两个值,这样比原始类型更加精确,让我们的值在原始类型至上,进一步限制,防止出现意料之外的相同原始类型的值。
除了字面量类型外,还可以使用字符串模板类型
可以通过模板字符串,来定制字面量类型的灵活性,这种类型叫做字符包模板类型
不过,很少见单独使用字面量类型,而是在使用联合类型的时候,组合使用。
联合类型可以理解为一组类型可用的集合,只要符合这些类型集合中的任意一个,就说明当前值是符合当前类型的,通常用 | 来分割不同的类型。比如上文的code
这个听起来很简单,但需要有几点
如果是函数类型,需要用()进行包裹
联合类型是可以在类型中进行嵌套的,但最后都会进行平铺到一级上去。
既然有联合类型的 | ,那么就会有交叉类型 & 。
交叉类型统一也是合集,使用 & 连接,但此时交叉类型会合并成一个类型,要求当前的值符合当前所有类型,不过,交叉类型使用场景少于联合类型,因为类型中的接口子类型是不可控的,如果存在同一字段,不同类型,那么会被合并为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 中,修饰符分为访问性修饰符,他们包括public 、 private 、protected 。还有一个readonly,属于操作性修饰符。
在类中,默认的修饰符是 public ,意味着我们可以自由访问被 public 标注的成员,包括在类、类的实例、甚至子类,都可以被访问到。
当成员被标记是 private 的时候,他不能在声明他的类外部进行访问,也就是说仅仅在当前类的逻辑里面的变量。
而对于 protected ,他的行为和 private 相似,但有一点不同,那就是可以在子类进行访问,当然实例和子类的实力依然是不可直接访问的。
readonly 与接口的 readonly 意义一致,不参与类型兼容的逻辑,但这里需要注意,在构造函数中是可以进行对 readonly 成员赋值的,即使 readonly 成员存在初始化值。
我们可以在构造函数中的参数上,使用修饰符,这样我们可以集中定义、初始化成员。
我们已经简单了解修饰符了,那我们发散一下,如果将一个类作为一个类型,成员和修饰符之间的区别,会影响类型的兼容吗?我们来实验一下,现在有以下代码:
我们看到,当成员相同,修饰符都是 public 的情况下,他们的类型是兼容的,可以相互赋值。
但如果修饰符进行一下改动呢?
从这个例子可以看出来,即使是修饰符相同,但是非 public 的情况下,只有子类的类型是可以兼容的,非子类的情况下,即使是结构一样,也是不兼容的,同样的 protected 也存在相同的逻辑。
静态成员
我们可以使用 static 关键字,来表示一个成员是静态成员,与需要修饰符访问的成员不同,静态成员无法通过this来访问,而是通过直接在访问类的属性、方法来获取。而实现这个逻辑的关键点,就是静态成员被挂载到当前的函数体上,而非成员被挂载到原型链上。因此静态成员不会被this访问到,也不会被实例继承,他仅仅是属于当前被定义的类。
静态成员之前可以增加修饰符,因此对于这些静态成员,也可以使用修饰符所代表的的逻辑。
继承
我们在前面也提到过子类,实际上就是依靠继承来实现的子类,严谨一点,父类被称为基类,子类被称为派生类,父类通过修饰符,来确定子类可以访问父类的哪些属性,同时,子类也可以覆盖父类的方法,也可以直接通过 super 调用父类的方法。
不过,看到这里,大家可能有疑问了,既然子类可以覆盖的方法,也可以调用父类的方法,但这样是不是太简单粗暴了,如果子类想要覆盖父类的某一个方法,但实际父类并没有此方法,那么子类就以外增加了一个全新的方法!
针对这个,typescript 提供了 override ,表示着尝试覆盖基类的方法,如果父类不存在对应的方法,那么不会通过类型检查。
抽象
抽象类同样是一个比较重要的概念,在概念广泛用于JAVA 等其他语言,意味着,是对类和方法的抽象,也就是说,抽象类描述了一个类应该有哪些成员、一个抽象方法描述了,这个方法在的入参类型和返回值类型。
不管是类还是成员,抽象用 abstract 关键字声明,没有此关键字的成员,不会作为抽象成员,而是作为正常的父类成员,不过,抽象类尽可能的不要声明静态成员的,因为按照逻辑,抽象类抽象的,是不存在的,既然不存在,那么就不存在可以直接访问的静态成员,尽管语法支持,但逻辑上是走不通的。
现在回顾一下访问性修饰符,他们包括public 、 private 、protected 。
但是这三种状态作用到构造函数上,会变成什么样的呢?
首先默认是 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 类型的变量可以再次被赋值为任意其它类型,但只能赋值给 any 与 unknown 类型的变量。
any 便不会出现这种情况,产生这种情况的原因就是 unknown 是任何类型的父类型,但 any 既是所有类型的父类型,又是所有类型的子类型。因此,当其他类型赋值给 unknown 类型的变量的时候,是可以做到兼容,但如果 unknown 赋值非 any 与 unknown 类型的变量的时候,就会抛出错误,因为父类型是不能赋值给子类型。因此这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用typeof、类型断言等方式来缩小未知范围
因此,在未知类型的时候,推荐语义和范围更加正确的 unknown 。不过既然包含任何类型的 unknown ,那么就存在什么都不包含的 never 类型。
说到不包含任何东西,我们很快能想到前文的 void ,但实际上,void 所代表的的不包含任何东西的类型,虽然不包含任何东西,但依然是一个类型,而 never 是连类型都是空的,他不携带任何类型信息,因此即使在联合类型中使用 never ,也会被忽略,他是所有类型的子类型,但只有 never 类型的变量才能赋值给另一个 never 变量,这里需要注意,即使是 any 也无法赋值给 never 。
never 类型的特性也意味着他不会出现在正常项目逻辑中,因此他的出现,都是出乎意料的、不合常规、不合逻辑的。
因此在抛出错误的语句中,我们才会使用 never 。如果抛出的错误是在一个函数中,那么函数的返回值也可以是 never ,因为抛出错误会导致函数停止执行,因此返回值就变成了死代码。
亦或者数组在初始化的时候,并没有给予类型,从而会被推导为 never[] 。
或者是永远无法触达的死代码,比如一个函数中,存在永远循环的while循环,那么他的返回值也会是 never ,因为死循环导致了返回值变成了死代码。
类型断言
我们在上文提到了断言,那么我们就在这稍微讲一下,断言就是显式告诉编译器:不要管你推导出来的类型,也不要管他所在的变量是什么类型,看这个断言他就是这个类型。
断言分为两种,一种是尖括号语法,另一种是as语法。
当然了,断言也可以让当前类型变成 any ,让代码再次跌入不可维护的状态,因此类型断言同 any 一样,尽可能地少用。
断言其实也不是说啥就是啥,如果断言的类型与之前差别过大,依然会被抛出类型错误。
根据提示,我们需要断言成 unknown ,然后再次断言成为我们想要的类型。
这种断言称为双重断言。由于两种类型并没有直接的从属关系,因此,需要我们来将他断言到最顶层类型,也就是 unknown 和 any ,之后再次断言为他们的类型,从而产生了双重断言。
也就是说,首次断言并非一定是 unknown 类型,而是只要是这两个类型的共同父类型,都可以进行双重断言。
也就说,类型是有层级关系。
还有一种断言,我们在使用 typescript 的时候经常用到,那就是非空断言。
存在一个对象,他的某个方法是可选参数,虽然我们增加了必要的可选成员,但当我们调用可选成员的时候,依然会提醒当前成员可能为空。
像这种情境,我们就可以使用非空断言,来确保编译正确,除了这种情境以外,还有querySelector等dom方法,可以使用非空断言,来确定自己肯定能查找到对应的元素。
小结
我们终于接触到了 typescript,这一章是入门章节,我们为了了解之后错综复杂的类型变换,不得不补充一些知识。而这些知识只是皮毛,我们在之后还会反反复复进行深挖,不断重新回过头来回顾这一章,从而达到由浅入深的目的。了解了这一章之后,我们再认识一下typescript提供的类型工具,就可以一头钻进这些类型相互缠绕的体操中了。那时候,我们就面各种类型交至出来的迷宫。但是,这并非我们的终点,而是我们新的起点。