TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
TypeScript 简介
什么是 TypeScript
TypeScript 是 JavaScript 的超集,TS 相比 JS 的优势:
- 更早发现错误,减少找/改 BUG 时间,提升开发效率
- 程序中任何位置的代码都有代码提示,随时随地的安全感,增强开发体验
- 强大的类型系统提升了代码的可维护性,使得重构代码更容易
- 支持最新的 ECMAScript 语法,优先体验最新的语法
- TS 类型推断机制,不需要在代码中的每个地方都显示标注类型
此外,Vue3 源码使用 TS 重写,Angular 默认支持 TS,React 与 TS 完美配合,TypeScript 已成为大中型前端项目的首选语言。
安装 TypeScript
由于 Node.js/浏览器只认识 JS 代码,所以需要将 TS 代码转换为 JS 代码,才能运行。
1 | # 安装 TypeScript |
以上命令会在全局环境下安装 tsc
命令,安装完成之后,我们就可以在任何地方执行 tsc
命令了
编译并运行 TypeScript
- 创建
hello.ts
文件
1 | function sayHello(person: string) { |
- 将 TS 编译为 JS
1 | $ tsc hello.ts |
- 执行 JS 代码
1 | $ node hello.js |
简化运行 TS
使用 ts-node
可以直接运行 TS 代码,不需要编译为 JS 代码
1 | $ npm install -g ts-node |
使用:
1 | $ ts-node hello.ts |
TypeScript 基础
原始数据类型
JavaScript 的类型
- 原始数据类型(Primitive data types):
Boolean
,Null
,Undefined
,Number
,String
,Symbol
,以及 ES10 中的新类型BigInt
- 原始数据类型(Primitive data types):
Object
TypeScript 新增类型
联合类型,自定义类型(类型别名),接口,元组,字面量类型,枚举,void,any 等
1 | let isDone: boolean = false; |
数组类型
数组类型有 2 种写法:「类型 + 方括号」表示法和数组泛型(Array Generic)
1 | let fibonacci: number[] = [1, 1, 2, 3, 5]; |
接口也可以用来描述数组:
1 | interface NumberArray { |
NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字
联合类型
1 | // 注意小括号 |
类型别名
类型别名(自定义类型):为任意类型起别名
使用场景:当复杂的同一类型被多次使用时,可以通过类型别名,简化该类型的使用
1 | type CustomArray = (number | string)[] |
函数类型
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):
1 | // 函数声明(Function Declaration) |
TypeScript 为函数指定类型的两种方式:
- 单独指定参数,返回值类型
1 | function add(num1: number, num2: number): number { |
1 | // ES6 箭头函数:https://es6.ruanyifeng.com/#docs/function#%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0 |
- 同时指定参数,返回值类型
1 | const add: (num1: number, num2: number) => number = (num1, num2) => { |
当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型
这种形式只适用于函数表达式
可选参数
使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,需要使用可选参数
1 | function buildName(firstName: string, lastName?: string) { |
可选参数只能出现来参数列表的最后,也就是说可选参数后面不允许再出现必需参数了
对象类型
JS 中的对象是由属性和方法构成的,而 TS 中对象的类型就是在描述对象的结构(有什么类型的属性和方法)
1 | let person: { name: string; age: number; sayHi(): void } = { |
- 直接使用
{}
来描述对象结构,属性采用属性名:类型
的形式;方法采用方法名():返回值类型
的形式 - 在一行代码中指定对象的多个属性类型时,使用
;
来分隔- 如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉
;
- 方法的类型也可以使用箭头函数形式(
{sayHi: () => void}
)
- 如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉
1 | let person: { |
可选属性
对象的属性或方法也可以是可选的,此时需要用到可选属性
1 | // 如果发送 GET 请求,method 属性就可以省略 |
接口
当一个对象类型被多次使用时,一般会使用接口来描述对象的类型,达到复用的目的。解释:
- 使用
interface
关键字来声明接口 - 接口名称可以是任意合法的变量名称
- 声明接口后,直接使用接口名称作为变量的类型
- 因为每一行只有一个属性类型,因此属性类型后面没有
;
1 | interface IPerson { |
- 相同点:都可以给对象指定类型
- 不同的:
- 接口 interface,只能为对象指定类型
- 类型别名 type,不仅可以为对象指定类型,实际上可以为任意类型指定别名
1 | interface IPerson { |
接口继承
如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用
1 | interface Point2D { x: number; y: number } |
更好的方式:
1 | interface Point2D { x: number; y: number } |
- 使用
extends
关键字实现了接口继承 - 继承后拥有了父接口的所有属性和方法
元组类型
使用场景:在地图中,经常用经纬度坐标来标记位置信息,可以使用数组来记录坐标,那么该数组中只有两个元素,并且这两个元素都是数值类型
1 | let position: number[] = [39.5427, 116.2317] |
使用 number[] 缺点:不严谨,因该类型的数组中可以出现任意多个数字。更好的方式:使用元素
1 | let position: [number, number] = [39.5427, 116.2317] |
元组类型可以确切地标记出有多少个元素,以及每个元素的类型
类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型
发生类型推论的两种场景场景:
- 声明变量并初始化时
- 决定函数返回值时
这两种情况下,类型注解可以省略不写
1 | let age = 18 // 推论为 number |
类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型,类型断言有两种语法:
1 | 值 as 类型 |
或
1 | <类型>值 |
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 值 as 类型
1 | interface Cat { |
字面量类型 Literal Types
1 | let str1 = 'Hello TS' |
此处的 str2
的类型为 'Hello TS'
,而不是 string
,因为 str2
是一个字面量类型,它的值只能是 'Hello TS'
,不能是其他值
除字符串外,任意的 JS 字面量(对象,数字等)都可以作为类型使用
使用场景:用来表示一组明确的可选值列表,字面量类型配合联合类型一起使用,比如在贪吃蛇游戏中,游戏方向的可选值只能是上,下,左,右中的任意一个:
1 | function changeDirection(direction: 'up' | 'down' | 'left' | 'right') { |
相比于 string 类型,使用字面量类型更近精确,严谨
枚举类型 Enum
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等
1 | enum Direction { Up, Down, Left, Right } |
数字枚举
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射,也可以给枚举中的成员初始化值
1 | // Down -> 11,Left -> 12,Right -> 13 |
1 | enum Direction { Up = 2, Down = 4, Left = 8, Right = 16 } |
字符串枚举
1 | enum Direction { |
字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值
当编译为 JS 代码时,其他类型会在编译时自动移除,但是枚举类型会被保留在 JS 代码中
1 | var Direction; |
枚举与字面量类型+联合类型组合的功能类似,都用了表示一组明确的可选值列表。一般情况下,推荐使用字面量类型+联合类型组合的方式,因为相比枚举,这种方式更近直观,简洁,高效
any 类型
不推荐使用 any!这会让 TypeScript 失去 TS 类型保护的优势
任意值(Any)用来表示允许赋值为任意类型
1 | let myFavoriteNumber: any = 'seven'; |
其他隐式具有 any 的情况:1. 声明变量不提供类型也不提供默认值 2. 函数参数不加类型
typeof
JS 中提供了 typeof
操作符,用来在 JS 中获取数据的类型
1 | console.log(typeof "Hello world") // string |
实际上,TS 也提供了 typeof
操作符:可以在类型上下文中引用变量或属性的类型
使用场景:根据已有变量的值,获取该值的类型,来简化类型书写
1 | let p = { x: 1, y: 2 } |
1 | let p = { x: 1, y: 2 } |
- 使用
typeof
操作符来获取变量 p 的类型,结果与第一种(对象字面量形式的类型)相同 typeof
出现在类型注解的位置(参数名称的冒号后面)所处的环境就在类型上下文(区别于 JS 代码)- 注意:
typeof
只能用来查询变量或属性的类型,无法查询其他形式的类型(比如函数调用的类型)
TypeScript 进阶
类 Class
TypeScript 全面支持 ES2015 中引入的 class
关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等)
类相关的概念:
类(Class)
:定义了一件事物的抽象特点,包含它的属性和方法对象(Object)
:类的实例,通过new
生成面向对象(OOP)的三大特性
:封装、继承、多态封装(Encapsulation)
:将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据继承(Inheritance)
:子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性多态(Polymorphism)
:由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
存取器(getter & setter)
:用以改变属性的读取和赋值行为修饰符(Modifiers)
:修饰符是一些关键字,用于限定成员或类型的性质。比如public
表示公有属性或方法抽象类(Abstract Class)
:抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现接口(Interfaces)
:不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
实例属性初始化:
1 | class Person { |
- 声明成员 age,类型为 number,没有初始值
- 声明成员 gender,并设置初始值,此时,可省略类型注解(TS 类型推论为 string 类型)
构造函数
1 | class Person { |
- 成员初始化后,才可以通过 this.age 来访问实例成员
- 需要为构造函数指定类型注解,否则会被隐式推断为 any;构造函数不需要返回值类型
实例方法
1 | class Point { |
类的继承
类继承的两种方式:
extends
继承父类implements
实现接口
JS 中只有 extends,而 implements 是 TS 提供的
1 | class Animal { |
- 通过
extends
关键字实现类的继承 - 子类 Dog 继承了父类 Animal ,则 Dog 的实例对象就同时具有了父类 Animal 和子类 Dog 的所有属性和方法
1 | interface ISingable { |
- 通过
implements
关键字让class
实现接口 - Person 类实现 Singable 意味着 Person 类中必须实现 ISingable 接口中的所有方法和属性
可见性修饰符
类成员可见性:可以使用 TS 来控制 class 的方法或属性对于 class 外的代码是否可见
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public
、privat
e 和 protected
public
:修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的private
:修饰的属性或方法是私有的,不能在声明它的类的外部访问(对子类和实例对象都不可见)protected
:修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的,对实例不可见
参数属性
修饰符和 readonly
还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁
只读属性关键字 readonly
,只允许出现在属性声明或索引签名或构造函数中,用来防止在构造函数之外对属性进行赋值
1 | class Person { |
- 使用
readonly
关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法 - 注意:属性 age 后面的类型注解(比如此处的 number)如果不加,则 age 的类型为 18(字面量类型)
- 接口或者 {} 表示的对象类型,也可以使用 readonly
- 如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面
类型兼容性
两种类型系统:
- 结构化类型系统 Structural Type System
- 标明类型系统 Nominal Type System
TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型),类型检查关注的是值所具有的形状
1 | class Point { x: number; y: number } |
- Point 和 Point2D 是两个名称不同的类,变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并且没有类型错误
- 因为 TS 是结构化类型系统,只检查Point 和 Point2D 的结构是否相同(都具有 x 和 y 两个属性,属性类型也相同)
- 但是,如果在 标明类型系统中(C#,Java 等),它们是不同的类,类型无法兼容
在结构化类型系统中,对于对象类型来说,y 的成员至少与 x 相同。则 x 兼容 y(成员多的可以赋值给少的)
1 | class Point { x: number; y: number } |
- Point3D 的成员至少与 Point 相同,则 Point 兼容 Point3D
- 因此,成员多的 Point3D 可以赋值给成员少的 Point
除了 class 之外,TS 中的其他类型也存在相互兼容的情况,包括
- 接口兼容性
- 函数兼容性等
接口兼容性
接口之间的兼容性类似于 class,并且 class 和 interface 之间也可以兼容
1 | interface Point { x: number; y: number } |
1 | interface Point2D { x: number; y: number } |
函数兼容性
函数兼容性需要考虑:
- 参数个数
- 参数类型
- 返回值类型
参数个数:参数多的兼容参数少的(参数少的可以赋值给多的)
1 | type F1 = (a: number) => void |
1 | const arr = ['a', 'b', 'c'] |
- 参数少的可以赋值给多的,因此 f1 可以赋值给 f2
- 数组 forEach 方法的第一个参数是回调函数,该示例中类型为:(value: string, index: number, array: string[]) => void
- 在 JS 中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性
- 并且因为回调函数是由类型的,所以 TS 会自动推导出参数 item,index,array 的类型
参数类型:相同位置的参数类型要相同(原始类型)或兼容(对象类型)
1 | type F1 = (a: number) => string |
函数类型 F2 兼容函数类型 F1,因为 F1 和 F2 的第一个参数类型相同
1 | interface Point2D { x: number; y: number } |
特别注意,此处与上述提到的接口兼容性冲突,这是参数少的 f2 可以赋值给参数多的 f3
返回值类型:只关注返回值类型本身即可
1 | type F5 = () => string |
- 如果返回值类型是原始类型,此时两个类型要相同
1 | type F7 = () => { name: string } |
- 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的
交叉类型
交叉类型(&):功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)
1 | interface Person { name: string } |
使用交叉类型后,新的类型 PersonDetail 就同时具备了 Person 和 Contact 的所有属性类型,相当于
1 | type PersonDetail = { name: string; phone: string } |
- 相同点:都可以实现对象类型的组合
- 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同
泛型 Generics
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
泛型在**保证类型安全(不丢失类型信息)**的同时,可以让函数等与多种不同的类型一起工作,灵活可复用
1 | function createArray<T>(length: number, value: T): Array<T> { |
- 语法:在函数名称的后面添加
<>
,尖括号中添加类型变量 - 上例中,我们在函数名后添加了
<T>
,其中 T 用来指代任意输入的类型,在后面的输入value: T
和输出Array<T>
中即可使用了 - 接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来
多个类型参数
定义泛型的时候,可以一次定义多个类型参数
1 | function swap<T, U>(tuple: [T, U]): [U, T] { |
泛型约束
泛型约束:默认情况下,泛型函数的类型变量 Type
可以代表多个类型,这导致无法访问任何属性
1 | function loggingIdentity<T>(arg: T): T { |
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:
1 | interface Lengthwise { |
- 使用
extends
关键字约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性 - 此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了