万万没想到会来到第六篇,第六篇写TypeScript。
Typescript
Typescript是JavaScript的超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上。
Typescript的优势:
Typescript增加了代码的可读性。
- 类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用了
- 可以在编译阶段就发现大部分错误,这总比在运行时候出错好
- 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等
TypeScript 非常包容
- TypeScript 是 JavaScript 的超集,
.js
文件可以直接重命名为.ts
即可 - 即使不显式的定义类型,也能够自动做出类型推论
- 可以定义从简单到复杂的几乎一切类型
- 即使 TypeScript 编译报错,也可以生成 JavaScript 文件
- 兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取
Typescript的劣势:
- 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念
- 短期可能会增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期维护的项目,TypeScript 能够减少其维护成本
- 集成到构建流程需要一些工作量
- 可能和一些库结合的不是很完美
安装和使用
使用typescript编写的文件以ts为文件后缀,用typescript编写react时以tsx为文件后缀。
安装typescript的命令行工具
1 | npm install -g typescript |
以上命令会在全局环境下安装tsc命令,安装完成后可以在任何地方执行tsc命令
编译typescript文件
1 | tsc hello.ts |
如果想要用typescript写node文件,则需要引入第三方声明文件:
1 | npm install @types/node --save-dev |
简单的编译示例:
hello.ts
1 | function sayHello(person: string) { |
执行
1 | tsc hello.ts |
编译生成的hello.js文件
1 | function sayHello(person) { |
typeScript 中,使用 :
指定变量的类型,:
的前后有没有空格都可以。
模块@types
DefinitelyTyped 是 TypeScript 最大的优势之一,社区已经记录了 90% 的顶级 JavaScript 库。你可以非常高效地使用这些库,而无须在单独的窗口打开相应文档以确保输入的正确性。
你可以通过 npm
来安装使用 @types
,例如为 jquery
添加声明文件:
1 | npm install @types/jquery --save-dev |
安装完之后,不需要特别的配置,你就可以像使用模块一样使用它:
1 | import * as $ from 'jquery'; |
编译上下文tsconfig.json
编译上下文算是一个比较花哨的术语,可以用它来给文件分组,告诉 TypeScript 哪些文件是有效的,哪些是无效的。除了有效文件所携带信息外,编译上下文还包含有正在被使用的编译选项的信息。定义这种逻辑分组,一个比较好的方式是使用 tsconfig.json
文件。
在项目的根目录下创建一个空 JSON 文件。通过这种方式,TypeScript 将 会把此目录和子目录下的所有 .ts 文件作为编译上下文的一部分,它还会包含一部分默认的编译选项。
你可以通过 compilerOptions
来定制你的编译选项:
1 | { |
数据类型与对象类型
typescript包含javascript的五种基本数据类型和ES6中声明的symbol,唯一的区别是在声明变量时需指明变量类型。
除此之外,typescript有新添加的类型
字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
1 | type EventNames = 'click' | 'scroll' | 'mousemove'; |
任意类型
任意值(Any)用来表示允许赋值为任意类型。如果是 any
类型,则允许被赋值为任意类型。
1 | let myFavoriteNumber: any = 'seven'; |
在任意值上访问任何属性都是允许的.如果变量在声明时未指定其类型,则被识别为任意类型。
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
1 | let myFavoriteNumber: string | number; |
上面的代码将myFavoriteNumber定义为字符串或者数值型,在不同的语句可以切换不同的类型,但不允许是定义以外的类型。
联合类型使用 |
分隔每个类型。当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型,并使用该类型
1 | let myFavoriteNumber: string | number; |
交叉类型
交叉类型可以把现有的类型组合起来得到新的类型,从而拥有全部的属性,表示为A & B
实例
1 | interface IPerson { |
交叉类型是两个类型的并集
条件类型
条件类型是在Typescrip在2.8版本加入的一个新featrue,用来表达非均匀类型,即基于某个条件下表示推断给定的可能的两种类型之一。
1 | type StringOnly<T> = T extends string ? never : T; |
模版类型
模版类型使用模版字符串的方式,将别的字面量类型作为type引入
1 | type World = "world"; |
typescript内置了一些模版类型,方便使用 大写、小写、首字母大写、首字母小写
1 | type Greeting = "Hello, world" |
类型别名
类型别名用来给一个类型起个新名字,常用于联合类型
1 | type Name = string; |
类型断言
类型断言可以
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
要使得 A
能够被断言为 B
,只需要 A
兼容 B
或 B
兼容 A
即可
类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。
断言类型有两种形式,其一是尖括号语法
1 | let someValue:any = 'this is a string'; |
另一种是as语法
1 | let someValue:any = 'this is a string'; |
类型断言在枚举值中的应用
如果写两个枚举值,在调用时通常需要使用类型断言来调用
1 | export enum PLAN_STATUS { |
类型推论
如果定义的时候有赋值,typescript会自动推测出一个类型;
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any
类型而完全不被类型检查;
1 | //定义时有赋值,自动推测出类型,之后赋值为别的类型会报错 |
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
1 | function createArray<T>(length: number, value: T): Array<T> { |
在函数名后添加 <T>
,其中 T
用来指代任意输入的类型,然后在后面的输入 value: T
和输出 Array<T>
中即可使用了。
接着在调用的时候,可以指定它具体的类型为 string
。或者也可以不手动指定,而让类型推论自动推算出来
也可以指定泛型的默认类型,这样如果调用时没用指定类型,则使用默认类型
1 | //在function中指定默认类型,在调用时没有指定的话即为默认类型 |
定义泛型时,也可以一次定义多个泛型类型参数,
1 | function swap<T, U>(tuple: [T, U]): [U, T] { |
虽然泛型没有指定数据结构,但是可以通过接口规定泛型的属性和方法,传入参数时进行属性校验,在内部操作时也可以直接操作属性而不会出现没有属性或者方法报错的情况。此外参数之间也可以互相继承。
1 | interface Lengthwise { |
箭头函数使用泛型
1 | const foo = <T>(x: T) => T; // Error: T 标签没有关闭 |
泛型接口与泛型类
泛型还可以用于定义接口和类
1 | interface CreateArrayFunc<T> { |
T
代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T
可以用任何有效名称代替。除了 T
之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
新增基本类型
元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
1 | let tom: [string, number] = ['Tom', 25]; |
当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。
1 | let tom: [string, number]; |
当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型
1 | let tom: [string, number]; |
枚举
TypeScript 的枚举类型的概念来源于C#
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
1 | export enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; |
默认情况下,枚举成员会被赋值为从 0
开始递增的数字,同时也会对枚举值到枚举名进行反向映射
1 | enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; |
外部枚举(Ambient Enums)是使用 declare enum
定义的枚举类型:
1 | declare enum Directions { |
declare
定义的类型只会用于编译时的检查,编译结果中会被删除。
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
enum和const enum
enum可以进行反向查找,所以遍历得到的长度是预计长度的两倍, const enum不可以进行反向查找,所以得到的是预计长度
1 | enum REVERSE{ |
never
never是typescript的底层类型,他常用于
1.不会有返回值的函数
2.总是抛出错误的函数
unkown
interface
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类或者对象去实现(implement)。
可配置属性
接口中可以包含确定属性、可选属性、任意属性、只读属性四种属性
确定属性是指变量由接口生成时,接口中的确定属性不能多,也不能少;
可选属性在接口中规定后,在变量中可以写可以不写;
任意属性是指在接口定义时允许变量自定义属性,这时要在接口中定义任意属性;任意属性的类型必须是确定属性和可选属性的母集,且一个接口只能使用一个任意属性,如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
只读属性是指对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly
定义只读属性。
typescript使用接口(Interfaces)来定义对象的类型。接口是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
1 | interface Person { |
上面的代码中,我们定义了一个接口 Person
,接着定义了一个变量 tom
,它的类型是 Person
。这样,我们就约束了 tom
的形状必须和接口 Person
一致。
一般情况下,定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的,会报错:
1 | let tom: Person = { |
可以设置可选属性、任意属性、只读属性。
可选属性为接口定义而对象可以不引用,
任意属性是接口不指定而对象可以添加,
只读属性是接口定义后在对象第一次初始化时添加,其后不能更改。
利用可选属性可以进行部分继承
1 | interface Person { |
可实现接口类型
接口可以定义对象,设置需要存在的普通属性
1 | interface Person { |
Interface 还可以用来规范函数的形状。Interface 里面需要列出参数列表返回值类型的函数定义。
1 | interface Func { |
interface 还可以用来定义可索引类型的接口,比如数组或者对象。需要注意的是 index 只能为 number 类型或 string 类型
1 | interface StringSet { |
接口除了定义变量,还可以在类中使用,用来实现类的共性接口。由类继承时一般同时定义静态属性接口和实例属性接口进行检查
1 | // PersonConstructor 是用来检查静态部分的 |
在同一个接口中可以同时定义多种类型,比如函数或者属性,继承该接口时所有的属性一起继承
1 | interface Counter { |
接口可以继承接口,可以继承父接口的所有方法
1 | interface PersonInfoInterface { // 第一个接口 |
接口还可以继承类,再由新类继承接口时同时也继承了接口所继承的类
1 | class Person { |
接口注意事项
接口(interface)定义了“公共(public)”契约(Contract),因此在接口(interface)上具有protected
或private
访问修饰符没有任何意义,更多的是实现细节
使用read-only访问修饰符
1 | interface IModuleMenuItem { |
接口和类的区别
接口只规定类的形状,也就是类具有哪些属性和方法,不具体实现这些属性和方法
实例
1 | interface ContentInterface{ |
类class
TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。
类的相关概念
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通过
new
生成 - 面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如
Cat
和Dog
都继承自Animal
,但是分别实现了自己的eat
方法。此时针对某一个实例,我们无需了解它是Cat
还是Dog
,就可以直接调用eat
方法,程序会自动判断出来应该如何执行eat
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如
public
表示公有属性或方法 - 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
类的属性和方法
类的继承
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public
、private
和 protected
。
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public
的private
修饰的属性或方法是私有的,不能在声明它的类的外部访问,在子类中也是不允许访问的。该类不允许被继承或者实例化:protected
修饰的属性或方法是受保护的,它和private
类似,区别是它在子类中也是允许被访问的,且该类只允许被继承,不能被实例化
abstract
用于定义抽象类和其中的抽象方法。抽象类是不允许被实例化的,抽象类中的抽象方法必须被子类实现:
声明语句与声明文件、声明合并
假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script>
标签引入 jQuery,然后就可以使用全局变量 $
或 jQuery
了。
但是在 ts 中,编译器并不知道 $
或 jQuery
是什么东西1:
这时,我们需要使用 declare var
来定义它的类型
通常我们会把声明语句放到一个单独的文件(jQuery.d.ts
)中,这就是声明文件。声明文件必需以 .d.ts
为后缀。一般来说,ts 会解析项目中所有的 *.ts
文件,当然也包含以 .d.ts
结尾的文件。所以当我们将 jQuery.d.ts
放到项目中时,其他所有 *.ts
文件就都可以获得 jQuery
的类型定义了。
假如仍然无法解析,那么可以检查下 tsconfig.json
中的 files
、include
和 exclude
配置,确保其包含了 jQuery.d.ts
文件。
TS可以在编译时自动生成.d.ts文件,只需要在tsconfig.json配置文件中开启即可
1 | { |
一般只有三种情况需要手动定义声明文件:
1.通过script标签引入第三方库
2.使用的第三方npm包没有提供声明文件
3.自己团队内比较优秀的js库或者插件,为了提升开发体验
声明文件只是对类型的定义,不能赋值
如果定义了同名的函数、类、接口,typescript会自动合并。接口的属性和方法都支持合并
1 | interface Alarm{ |
合并时属性可以重复,但是不能有冲突,否则会报错。
1 | interface Alarm{ |
类的合并和接口的合并类似
对于没有提供声明文件的npm包,可以创建一个types目录,来管理自己写的声明文件,同时在配置文件tsconfig.json中的paths和baseUrl配置
1 | { |
npm包的声明文件主要有以下几种语法
1 | export const/let |
命名空间
在 JavaScript 使用命名空间时, 这有一个常用的、方便的语法:
1 | (function(something) { |
在确保创建的变量不会泄漏至全局命名空间时,这种方式在 JavaScript 中很常见。当基于文件模块使用时,你无须担心这点,但是该模式仍然适用于一组函数的逻辑分组。因此 TypeScript 提供了 namespace
关键字来描述这种分组,
1 | namespace Utility { |
值得注意的一点是,命名空间是支持嵌套的。因此,你可以做一些类似于在 Utility
命名空间下嵌套一个命名空间 Messaging
的事情。