虽然之前一直项目当中使用 Angular
来进行业务开发,但是总感觉对于 TypeScript
这一块的内容还是只停留在比较浅的使用层面,而最近又开始涉及到 React
相关内容,发现 React + TypeScript
算是业界标配了,所以就打算抽些时间深入的学习一下 TypeScript
,在这里记录记录,也算是查漏补缺吧
针对于相关内容会分为两篇文章来进行梳理,本文当中主要梳理一些基础内容,也算是回顾一下 TypeScript
的基本用法,而在下一章 深入 TypeScript 当中我们则会来看一些 TypeScript
当中的进阶内容
本文当中涉及到的内容可能比较零散,因为便于自己回顾,一些比较熟络的知识点可能会一笔带过,对于想了解整体流程的同学可以参考 官方文档 来了解更多
什么是 TypeScript
TypeScript
是 JavaScript
的一个超集,主要提供了『类型系统』和对 ES6
的支持,它与 JavaScript
的基本区别有以下这些
TypeScript | JavaScript |
---|---|
JavaScript 的超集用于解决大型项目的代码复杂性 |
一种脚本语言,用于创建动态网页 |
可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
最终被编译成 JavaScript 代码,使浏览器可以理解 |
可以直接在浏览器中使用 |
支持模块、泛型和接口 | 不支持模块,泛型或接口 |
社区的支持仍在增长,而且还不是很大 | 大量的社区支持以及大量文档和解决问题的支持 |
而函数之间的区别则有以下这些
TypeScript | JavaScript |
---|---|
含有类型 | 无类型 |
箭头函数 | 箭头函数(ES2015 ) |
函数类型 | 无函数类型 |
必填和可选参数 | 所有参数都是可选的 |
默认参数 | 默认参数 |
剩余参数 | 剩余参数 |
函数重载 | 无函数重载 |
当然任何事物都是有两面性的,通常来说 TypeScript
的一些弊端在于
- 有一定的学习成本,需要理解接口(
Interfaces
)、泛型(Generics
)、类(Classes
)、枚举类型(Enums
)等概念 - 短期可能会增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期维护的项目,
TypeScript
能够减少其维护成本 - 集成到构建流程需要一些工作量
- 可能和一些库结合的不是很完美
另外,除了 TypeScript
还有一个 Flow 可供选择,Flow
是 Facebook
出品的 JavaScript
静态类型检查工具,它与 Typescript
不同的是,它可以部分引入,不需要完全重构整个项目,所以对于一个已有一定规模的项目来说,迁移成本更小,也更加可行,所以还是根据团队和项目的情况判断是否需要使用 TypeScript
另外我们再来简单的了解一下 TypeScript
的工作流程,这有助于我们更好的理解 TypeScript
,其实简单来说,典型的 TypeScript
工作流程是下面这样的
在上图当中包含了三个 TypeScript
文件,而这些文件将被 TypeScript
编译器,根据配置的编译选项编译成对应三个不同的 .js
文件,对于大多数使用 TypeScript
开发的 Web
项目来说,我们还会对编译生成的文件进行打包处理,然后在进行部署
下面我们就来看看 TypeScript
当中的常用类型和一些基本概念,主要包括
- 基础类型
- 内置对象
- 联合类型
- 对象的类型(接口)
- 数组的类型
- 函数的类型
另外需要注意一点,如果本文当中未特殊指明示例是错误示范的话,则默认是编译通过
TypeScript 基础类型
我们先来看看 TypeScript
当中的一些基础类型,也是我们在平常经常见到的一些类型
布尔值
在 TypeScript
中,使用 boolean
定义布尔值类型(注意区分大小写)
1 | let x: boolean = false |
但是需要注意的是,如果使用的是构造函数 Boolean
创造的对象则不是布尔值
1 | // ❌ |
事实上 new Boolean()
返回的是一个 Boolean
对象
1 | // ✅ |
在 TypeScript
中,boolean
是 JavaScript
中的基本类型,而 Boolean
是 JavaScript
中的构造函数,其他基本类型(除了 null
和 undefined
)也是一样的
数值
使用 number
可以来定义数值类型,并且二进制与八进制等均可以使用
1 | let x: number = 6 |
字符串
使用 string
来定义字符串类型
1 | let x: string = 'zhangsan' |
可以发现,对于 ES6
当中的模板字符串同样适用
任意值
任意值(Any
)用来表示允许赋值为任意类型,与原始数据类型进行比对的话,如果是一个普通类型,在赋值过程中改变类型是不被允许的
1 | // ❌ |
但如果是 any
类型,则允许被赋值为任意类型
1 | // ✅ |
同时,在任意值上访问任何属性都是允许的
1 | let x: any = 'zhangsan' |
也允许调用任何方法
1 | let x: any = 'lisi' |
可以认为,『声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值』,但是如果变量在声明的时候,未指定其类型,那么它会被识别为任意值类型,比如下面示例当中的 x
等价于 x: any
1 | let x |
虽然 any 使用起来很方便,但是可想而知,如果我们大量使用 any
类型,就无法使用 TypeScript
所提供的大量的保护机制,为了解决 any
带来的问题,TypeScript 3.0
引入了 unknown
类型
Unknown
就像所有类型都可以赋值给 any
,所有类型也都可以赋值给 unknown
,这使得 unknown
成为 TypeScript
类型系统的另一种顶级类型(另一种是 any
),下面我们来看一下 unknown
类型的使用示例
1 | let value: unknown |
可以发现,我们对 value
变量的所有赋值都被认为是类型正确的,下面我们再来看看将类型为 unknown
的值赋值给其他类型的变量时会发生什么
1 | let value: unknown |
通过上面的示例可以发现,unknown
类型只能被赋值给 any
类型和 unknown
类型本身,其实也能理解,那就是只有能够保存任意类型值的容器才能保存 unknown
类型的值,毕竟我们不知道变量 value
中存储了什么类型的值
我们下面再来看看对类型为 unknown
的值执行操作时会发生什么
1 | let value: unknown |
将 value
变量类型设置为 unknown
后,这些操作都不再被认为是类型正确的,通过将 any
类型改变为 unknown
类型,我们已将允许所有更改的默认设置,更改为禁止任何更改
空值
某种程度上来说,void
类型像是与 any
类型相反,它表示没有任何类型,当一个函数没有返回值时,你通常会见到其返回值类型是 void
1 | function alertName(): void { |
声明一个 void
类型的变量没有什么用,因为你只能将它赋值为 undefined
和 null
,但是需要注意,在非严格模式下,变量的值可以为 undefined
或 null
,而严格模式下,变量的值只能为 undefined
,所以使用场景较多的还是针对于没有返回值的函数
1 | let x: void = undefined |
但是这里有一个需要注意的地方,即当定义的函数返回值为空值 void
的时候,虽然在该函数内部写 return
时编译会报错,但是依然可以编译成功
Null 和 Undefined
在 TypeScript
中,可以使用 null
和 undefined
来定义这两个原始数据类型
1 | let u: undefined = undefined |
与 void
的区别是,undefined
和 null
是所有类型的子类型,也就是说 undefined
类型的变量,可以赋值给 number
或 string
类型的变量
1 | // ✅ |
而 void
类型的变量不能赋值给 number
或 string
类型的变量
1 | // ❌ |
但是默认情况下像上面那样操作,编译器会提示错误,这是因为 tsconfig.json
里面有一个配置项是默认开启的
1 | // tsconfig.json |
其中 strictNullChecks
参数用于新的严格空检查模式,在严格空检查模式下,null
和 undefined
值都不属于任何一个类型,它们只能赋值给自己这种类型或者 any
object,Object 和 { }
我们这里主要看看这三者之间的区别,注意区分前两者的大小写
object
类型
object
类型是 TypeScript 2.2
引入的新类型,它用于表示非原始类型
1 | // node_modules/typescript/lib/lib.es5.d.ts |
Object
类型
Object
类型是所有 Object
类的实例的类型,它由以下两个接口来定义,其中 Object
接口定义了 Object.prototype
原型对象上的属性
1 | // node_modules/typescript/lib/lib.es5.d.ts |
而 ObjectConstructor
接口定义了 Object
类的属性
1 | // node_modules/typescript/lib/lib.es5.d.ts |
这里需要注意的是,Object
类的所有实例都继承了 Object
接口中的所有属性
{ }
类型
{ }
类型描述了一个没有成员的对象,当你试图访问这样一个对象的任意属性时,TypeScript
会产生一个编译时错误
1 | const obj = { } |
但是我们仍然可以使用在 Object
类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript
的原型链隐式地使用
1 | const obj = { } |
Never
never
类型表示的是那些永不存在的值的类型,例如 never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型(这个类型一般很少会用到,了解即可)
1 | // 返回 never 的函数必须存在无法达到的终点 |
在 TypeScript
中,可以利用 never
类型的特性来实现全面性检查,具体示例如下
1 | type Foo = string | number |
注意在 else
分支里面,我们把收窄为 never
的 foo
赋值给一个显示声明的 never
变量,如果一切逻辑正确,那么这里应该能够编译通过,但是如果我们修改了 Foo
的类型为 type Foo = string | number | boolean
但是忘记了修改 controlFlowAnalysisWithNever
方法中的控制流程
这时候 else
分支的 foo
类型会被收窄为 boolean
类型,导致无法赋值给 never
类型,这时就会产生一个编译错误,通过这个方式,我们可以确保 controlFlowAnalysisWithNever
方法总是穷尽了 Foo
的所有可能类型
通过这个示例,我们可以得出一个结论,即可以使用 never
来避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码,但是这里我们也需要注意区分 never
和 void
两者之间的区别
void
表示没有任何类型(可以被赋值为null
和undefined
)never
表示一个不包含值的类型,即表示永远不存在的值- 拥有
void
返回值类型的函数能正常运行,拥有never
返回值类型的函数无法正常返回,无法终止,或会抛出异常
内置对象
其实在上面介绍的一些原始数据类型,本质上它们都是 JavaScript
当中的 内置对象,它们已经在 TypeScript
中定义好了对应的类型,直接进行使用就行,内置对象是指根据标准在全局作用域(Global
)上存在的对象,这里的标准是指 ECMAScript
和其他环境(比如 DOM
)的标准
ECMAScript
标准提供的内置对象有 Boolean
、Error
、Date
、RegExp
等,我们可以在 TypeScript
中将变量定义为这些类型
1 | let b: Boolean = new Boolean(1) |
而常见的 DOM
和 BOM
提供的内置对象有 Document
、HTMLElement
、Event
、NodeList
等,我们在开发过程当中也会经常用到这些类型
1 | let body: HTMLElement = document.body |
而对于内置对象的定义文件,则在 TypeScript 核心库 的定义文件中,其中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript
中的,当我们在使用一些常用的方法的时候,TypeScript
实际上已经帮我们做了很多类型判断的工作了,比如
1 | // ✅ |
在上面的例子中,Math.pow()
必须接受两个 number
类型的参数,它的类型定义如下
1 | interface Math { |
再来看一个 DOM
中的例子
1 | // ❌ |
在上面的例子中,addEventListener
方法是在 TypeScript
核心库中定义的
1 | interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent { |
所以 e
被推断成了 MouseEvent
,而 MouseEvent
是没有 targetCurrent
属性的,所以报错了
此外我们还需要需要注意一点,那就是 TypeScript
核心库的定义中是不包含 Node.js
部分的,如果想用 TypeScript
写 Node.js
,则需要引入第三方声明文件
1 | npm install @types/node --save-dev |
TypeScript 断言
我们在有时候可能会遇到这种情况,那就是我们会比 TypeScript
更了解某个值的详细信息,通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型,在这种情况下我们就可以通过类型断言这种方式可以告诉编译器,我知道自己在干什么,类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构,它没有运行时的影响,只是在编译阶段起作用
类型断言
类型断言主要有两种形式,即 <>
和 as
,我们先来看看 <>
的形式
1 | let x: any = 'abc' |
等同于
1 | let x: any = 'abc' |
但是建议尽量使用 as
来替 <>
表示类型断言,因为
- 在
TypeScript
可以使用<>
来表示类型断言,但是在结合JSX
的语法时将带来解析上的困难,因此TypeScript
在.tsx
文件里禁用了使用<>
的类型断言 - 另外,
as
操作符在.ts
文件和.tsx
文件里都可以使用
非空断言
如果在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 !
可以用于断言操作对象是非 null
和非 undefined
类型,简单来说比如 x!
就是将从 x
值域中排除 null
和 undefined
,下面我们先来看几个非空断言操作符的一些使用场景
- 第一种情况,忽略
undefined
和null
类型
1 | function myFunc(maybeString: string | undefined | null) { |
- 第二种情况,调用函数时忽略
undefined
类型
1 | type NumGenerator = () => number |
因为 !
非空断言操作符会从编译生成的 JavaScript
代码中移除,所以在实际使用的过程中,要特别注意,比如下面这个例子
1 | const a: number | undefined = undefined |
以上 TypeScript
代码会编译生成以下 ES5
代码
1 | 'use strict' |
虽然在 TypeScript
代码中,我们使用了非空断言,使得 const b: number = a!
语句可以通过 TypeScript
类型检查器的检查,但在生成的 ES5
代码中,因为 !
非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined
确定赋值断言
在 TypeScript 2.7
版本中引入了『确定赋值断言』,即允许在实例属性和变量声明后面放置一个 !
号,从而告诉 TypeScript
该属性会被明确地赋值,来看下面这个示例
1 | let x: number |
运行后我们可以发现,错误提示显示说变量 x
在赋值前已经被使用了,为了解决这个问题,我们可以使用确定赋值断言
1 | let x!: number |
我们通过 let x!: number
来确定赋值断言,这样一来 TypeScript
编译器就会知道该属性会被明确地赋值
联合类型
联合类型(Union Types
)表示取值可以为多种类型中的一种,使用 |
分隔每个类型
1 | let x: string | number |
上面示例当中的 let x: string | number
含义是允许 x
的类型是 string
或者 number
,但是不能是其他类型
当 TypeScript
不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法
1 | // ❌ |
因为 length
不是 string
和 number
类型的共有属性,所以会报错,但是访问两者的的共有属性是没问题的,比如 toString()
方法
1 | // ✅ |
并且联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型
1 | let x: string | number |
上例中,在赋值为 'zhangsan'
的时候,x
被推断成了 string
,所以可以访问它的 length
属性,但是赋值为 7
的时候 x
被推断成了 number
,所以访问它的 length
属性时就会报错
关于联合类型,这里我们来看一个它的相关应用场景,也就是『类型保护函数』,要自定义一个类型保护,只需要简单地为这个类型保护定义一个函数即可,这个函数的返回值是一个『类型谓词』
类型谓词的语法为 parameterName is Type
这种形式,其中 parameterName
必须是当前函数签名里的一个参数名,来看下面这个示例
1 | interface Bird { |
在上面示例当中,当我们使用联合类型时,如果不用类型断言,默认只会获取两者共有的部分,在这种情况下,我们就可以采用类型谓词
1 | interface Bird { |
另外,我们可以借住 never
来区分的联合类型,比如下面这个示例
1 | enum KindType { |
所以这里可以利用 never
来进行完善
1 | function area(s: Shape) { |
更多可见 TypeScript 中的 never 类型具体有什么用?
对象的类型(接口)
在 TypeScript
中,我们使用接口(Interfaces
)来定义对象的类型,接口(Interfaces
)在面向对象语言中是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes
)去实现(implement
),我们先来看一个简单的示例
1 | interface Person { |
我们定义了一个接口 Person
,接着定义了一个变量 user
,它的类型是 Person
,这样我们就约束了 user
的结构必须和接口 Person
一致,但是需要注意的是,定义的变量比接口多一些或是少了一些属性都是不允许的
1 | // ❌ |
所以说,在赋值的时候,变量的结构必须和接口的结构保持一致
可选属性
有时候我们又希望不要完全匹配一个接口,那么这种情况下可以使用可选属性
1 | interface Person { |
但是此时仍然不允许添加未定义的属性
1 | interface Person { |
任意属性
有时候我们希望一个接口允许有任意的属性,可以使用 [propName: type]
的方式来来进行定义
1 | interface Person { |
但是这里有一个需要注意的地方,那就是『一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集』,比如下面这个示例就会报错
1 | // ❌ |
这里我们将任意属性的值允许是 string
,但是可选属性 age
的值却是 number
,number
不是 string
的子属性,所以报错了,通常来说一个接口中只能定义一个任意属性,如果接口中有多个类型的属性,则可以采用联合类型的方式
1 | interface Person { |
另外需要注意的一点就是『索引签名』参数类型必须为 string
或 number
』
1 | // ❌ |
上面示例是会报错的,这是因为 TypeScript
只支持两种索引签名,那就是字符串和数字,虽然可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型,这是因为当使用 number
来索引时,JavaScript
会将它转换成 string
然后再去索引对象,比如下面这个示例就不会报错
1 | // ✅ |
其实在上面示例当中和写成 '1': 1
是完全一样的,即使我们定义的是 [propName: number]
,这是因为 JavaScript
中对象的数字索引,最终会转成字符串来取值的,比如使用 100
(number
)去索引等同于使用 '100'
(string
)去索引,因此两者需要保持一致
只读属性
有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly
定义只读属性
1 | interface Person { |
在上面示例当中,我们使用 readonly
定义的属性 id
初始化后,又被赋值了,所以报错了,但是这里特别需要注意的一点就是『只读的约束是存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候』,比如下面这个示例
1 | interface Person { |
上例中有两处报错,第一处是在对 user
进行赋值的时候,没有给 id
赋值,第二处是在给 user.id
赋值的时候,由于它是只读属性,所以报错了
数组的类型
在 TypeScript 中,数组类型有多种定义方式,比较灵活,最简单的方法是使用『类型 + 方括号』来表示数组
1 | let x: number[] = [1, 2, 3, 4, 5] |
并且定义以后,数组的一些方法的参数也会根据数组在定义时约定的类型进行限制
1 | let x: number[] = [1, 2, 3, 4, 5] |
在上面示例当中,由于 push
方法只允许传入 number
类型的参数,但是却传了一个字符串类型的 6
,所以报错了,另外我们也可以使用数组泛型(Array Generic
) Array<elemType>
来表示数组
1 | let x: Array<number> = [1, 2, 3, 4, 5] |
关于泛型的相关内容,我们会在后面章节当中详细来进行介绍,下面我们来看看如何使用接口来描述数组
1 | interface NumberArray { |
虽然接口也可以用来描述数组,但是我们一般不会这么来使用,因为这种方式比前两种方式要复杂许多,不过有一种情况比较特殊,那就是它常用来表示类数组,类数组(Array-like Object
)不是数组类型,比如 arguments
1 | // ❌ |
由于 arguments
实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口
1 | interface Args { |
在这个例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length
和 callee
两个属性,事实上常用的类数组都有自己的接口定义,如 IArguments
,NodeList
,HTMLCollection
等
1 | function sum() { |
其实也就是我们之前提到过的『内置对象』,其中 IArguments
是 TypeScript
中定义好了的类型,它实际上就是
1 | interface IArguments { |
对于数组当中既存在数字又含有字符串的情况,我们可以考虑使用联合类型
1 | let x: (number | string)[] = [1, '2', 3] |
另外还有一种比较复杂的情况,那就是对象类型的数组,偷懒的话当然可以直接使用 any
,但是如若结构不算太过复杂的话可以使用下面这种方式
1 | const x: { name: string, age: number }[] = [{ name: 'zhangsan', age: 18 }] |
还可以将上面的写法简化一下,利用类型别名的方式
1 | type User = { name: string, age: number } |
函数的类型
在 JavaScript
中,有两种常见的定义函数的方式,即函数声明(Function Declaration
)和函数表达式(Function Expression
)
1 | // 函数声明(Function Declaration) |
一个函数有输入和输出,要在 TypeScript
中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单
1 | function add(x: number, y: number): number { |
但是输入多余的(或者少于要求的)参数,是不被允许的
1 | add(1, 2, 3) // ❌ |
但是如果我们要对一个函数表达式(Function Expression
)来定义的话,可能会写成这样
1 | let add = function (x: number, y: number): number { |
虽然是可以通过编译的,但是上面的代码其实只对等号右侧的匿名函数进行了类型定义,而等号左边的 add
,是通过赋值操作进行类型推论而推断出来的,如果需要我们手动给 add
添加类型,则应该是这样
1 | let add: (x: number, y: number) => number = function (x: number, y: number): number { |
但是这里注意不要混淆了 TypeScript
中的 =>
和 ES6
当中的箭头函数(=>
),在 TypeScript
的类型定义中,=>
用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型
使用接口
我们也可以使用接口的方式来定义一个函数需要符合的结构
1 | interface SearchFunc { |
采用函数表达式定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变
可选参数
与接口中的可选属性类似,我们用 ?
来表示可选的参数
1 | function buildName(firstName: string, lastName?: string) { |
不过需要注意的是,可选参数必须接在必需参数后面,也就是说可选参数后面不允许再出现必需参数了
1 | // ❌ |
默认值
在 ES6
中,我们允许给函数的参数添加默认值,TypeScript
会将添加了默认值的参数识别为可选参数
1 | function buildName(firstName: string, lastName: string = 'lisi') { |
但是需要注意一种情况,那就是我们在解构一个函数的时候,即给变量声明类型的同时又给变量设置默认值的情况,如下
1 | // ❌ |
如上,在这种情况下,编辑器会提示我们找不到名称 x
,针对于这种情况,我们可以像下面这样来进行处理
1 | // ✅ |
剩余参数
在 ES6
中,我们可以使用 ...rest
的方式获取函数中的剩余参数(rest
参数)
1 | function push(array, ...items) { |
事实上,items
是一个数组,所以我们可以用数组的类型来定义它
1 | function push(array: number[], ...items: number[]) { |
这里需要注意的是,rest
参数同可选参数一样,只能是最后一个参数
重载
在 Java
等面向对象语言当中的函数重载,指的是两个或者两个以上的同名函数,参数类型不同或者参数个数不同,它的好处是不需要为功能相似的函数起不同的名称,而在 TypeScript
当中,表现为给同一个函数提供多个函数类型定义,适用于接收不同的参数和返回不同结果的情况
TypeScript
在实现函数重载的时候,要求定义一系列的函数声明,在类型最宽泛的版本中实现重载(前面的是函数声明,目的是约束参数类型和个数,最后的函数实现是重载,表示要遵循前面的函数声明,一般在最后的函数实现时用 any
类型),不过函数重载在实际应用中使用的比较少,一般会用联合类型或泛型代替,并且函数重载的声明只用于类型检查阶段,在编译后会被删除
TypeScript
编译器在处理重载的时候,会去查询函数申明列表,从上至下直到匹配成功为止,所以要把最容易匹配的类型写到最前面
1 | function attr(val: string): string |
上面的写法声明完函数后,必须实现函数重载,也可以『只声明函数』
1 | // 后写的接口中的函数声明优先级高 |
声明合并
这里既然提及到了同名接口合并,我们就再来扩展一些,其实这也就是所谓的『声明合并』,不光是函数,在接口当中也是可以进行合并的,如果定义了两个相同名字的函数、接口或类,那么它们将会合并成一个类型,如下
1 | interface Alarm { |
上面的示例相当于
1 | interface Alarm { |
但是需要注意的是,合并的属性的类型必须是唯一的
1 | interface Alarm { |
上面示例当中虽然字段 price
重复了,但是类型都是 number
,所以不会报错,而下面这个示例则会编译错误
1 | interface Alarm { |
因为上面示例当中需要合并的类型不一致,所以报错了,另外接口中方法的合并,与函数的合并一样
1 | interface Alarm { |
相当于
1 | interface Alarm { |
重载与重写
最后我们再来看一个可能会与重载(overload
)弄混淆的概念,那就是重写(override
),这里需要注意区分两者之间的差异
- 重写是指子类重写『继承』自父类中的方法,虽然
TypeScript
和Java
相似,但是TypeScript
中的继承本质上还是JavaScript
中的『继承』机制(也就是原型链机制) - 而重载是指为同一个函数提供多个类型定义
1 | class Animal { |
1 | function double(val: number): number |
继承与多态
既然提到了继承,那我们就再来看一个与其十分类似的概念,那就是多态
- 继承,子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态,由继承而产生了相关的不同的类,对同一个方法可以有不同的响应
1 | class Animal { |
运算符
我们在上面的章节当中已经简单介绍过了可选属性和可选参数相关内容,在本小节当中我们再来稍微深入一些,来了解一些 TypeScript
当中的运算符
?. 运算符
TypeScript 3.7
实现了『可选链』(Optional Chaining
)的功能,有了可选链后,我们编写代码时如果遇到 null
或 undefined
就可以立即停止某些表达式的运行,可选链的核心是新的 ?.
运算符
1 | a?.b |
下面我们通过一个可选的属性访问的详细示例例子来进行了解
1 | const val = a?.b |
为了更好的理解,我们可以看一下上面示例对应编译生成的 ES5
代码
1 | var val = a === null || a === void 0 ? void 0 : a.b |
上述的代码会自动检查对象 a
是否为 null
或 undefined
,如果是的话就立即返回 undefined
,这样就可以立即停止某些表达式的运行,所以我们可以利用 ?.
来替代很多使用 &&
执行的空检查代码
1 | if (a && a.b) { } |
而 if (a?.b) { }
编译后的 ES
代码是下面这样的
1 | if ( |
但需要注意的是,?.
与 &&
运算符行为略有不同,&&
专门用于检测 falsy
值,比如空字符串、0
、NaN
、null
和 false
等,而 ?.
只会验证对象是否为 null
或 undefined
,对于 0
或空字符串来说,并不会出现所谓的『短路』
可选链除了支持可选属性的访问之外,它还支持可选元素的访问,它的行为类似于可选属性的访问,只是可选元素的访问允许我们访问非标识符的属性,比如任意字符串、数字索引和 Symbol
1 | function tryGetArrayElement<T>(arr?: T[], index: number = 0) { |
以上代码经过编译后会生成以下 ES5
代码
1 | 'use strict' |
通过观察生成的 ES5
代码,很明显在 tryGetArrayElement
方法中会自动检测输入参数 arr
的值是否为 null
或 undefined
,从而保证了我们代码的健壮性
另外,当我们尝试调用一个可能不存在的方法时也可以使用可选链,使用可选链可以使表达式自动返回 undefined
而不是抛出一个异常,比如
1 | let result = obj.customMethod?.() |
该 TypeScript
代码编译生成的 ES5
代码如下
1 | var result = (_a = obj.customMethod) === null |
另外在使用可选调用的时候,我们要注意以下两个注意事项
- 如果存在一个属性名且该属性名对应的值不是函数类型使用
?.
仍然会产生一个TypeError
异常 - 可选链的运算行为被局限在属性的访问、调用以及元素的访问,因为它不会沿伸到后续的表达式中,也就是说可选调用不会阻止
a?.b / someMethod()
表达式中的除法运算或someMethod
的方法调用
?? 运算符
在 TypeScript 3.7
版本中除了引入了前面介绍的可选链 ?.
之外,也引入了一个新的逻辑运算符,那就是『空值合并运算符(??
)』,当左侧操作数为 null
或 undefined
时,其返回右侧的操作数,否则返回左侧的操作数
与逻辑或 ||
运算符不同,逻辑或会在左操作数为 falsy
值时返回右侧操作数,也就是说如果你使用 ||
来为某些变量设置默认的值时,你可能会遇到意料之外的行为,比如为 falsy
值(''
、NaN
或 0
)时,下面来看一个具体的示例
1 | const foo = null ?? 'default string' |
以上 TypeScript
代码经过编译后,会生成以下 ES5
代码
1 | 'use strict' |
通过观察以上代码,我们更加直观的了解到,空值合并运算符是如何解决前面 ||
运算符存在的潜在问题,下面我们来看看空值合并运算符的特性和使用时的一些注意事项
- 短路
当空值合并运算符的左表达式不为 null
或 undefined
时,不会对右表达式进行求值
1 | function A() { |
上述代码运行后,控制台会输出以下结果
1 | A was called |
- 不能与
&&
或||
操作符共用
若空值合并运算符 ??
直接与 AND
(&&
)和 OR
(||
)操作符组合使用 ??
是不行的,这种情况下会抛出 SyntaxError
1 | // '||' and '??' operations cannot be mixed without parentheses.(5076) |
但当使用括号来显式表明优先级时是可行的
1 | (null || undefined) ?? 'foo' // => foo |
- 与可选链操作符
?.
的关系
空值合并运算符针对 undefined
与 null
这两个值,可选链式操作符 ?.
也是如此,可选链式操作符对于访问属性可能为 undefined
与 null
的对象时非常有用
1 | interface Customer { |
前面我们已经介绍了空值合并运算符的应用场景和使用时的一些注意事项,该运算符不仅可以在 TypeScript 3.7
以上版本中使用,当然也可以借助 Babel
来在 JavaScript
的环境中使用它,Babel 7.8.0
版本开始支持空值合并运算符
总结
以上就是我们梳理的一些 TypeScript
当中的基础内容,在下一章 深入 TypeScript 当中我们会接着来了解一些 TypeScript
当中的进阶内容,比如元祖,枚举,类等