对于前端从业者来说,TypeScript(以下简称 TS)已经不算是新技术。
Vue3 的源码基于 TS 编写, Angular 项目默认支持 TS 等。它出现的频率越来越高,而学习难度并不大,大概一个周末可以熟悉。
投入少,产出大,所以最好还是花点时间学习一下。
本文根据 TS handbook 整理出 TS 关键的知识点,并对某些部分作了扩展,希望能帮助大家学习理解。
前言
在学习 TS 之前,需要理解它的两个特点:
- TS 是 JS 的超集
- TS 给 JS 带来类型系统
这意味着 TS 的根基是 JS,是在 JS 的 基础上添加了类型系统。
类型声明
类型系统的一个特点是使用类型声明。
观察上述代码,在 JS 中,使用「等号=」给变量赋值。
经过 TS 的扩展,可以使用「冒号:」给变量赋类型。代码中声明变量 foo 的类型是 string。
类型校验
类型系统的另一个特点是进行类型校验。
在 TS 中,需要校验「等号=」左右的类型是否匹配。
如上述代码所示,值存在明确的类型,TS 会校验左右两侧的类型是否匹配,若不匹配则提示错误。
在了解这些前置知识后,来看看具体的 TS 知识点。
一、基本类型
继承 JS 的基本类型如:string、number、boolean、undefined、null。扩展了其他基本类型如:any、never、void。
1.1 string
表示类型为字符串
1.2 number
表示类型为数字
1.3 boolean
表示类型为布尔类型
1.4 undefined
表示类型为 undefined
1.5 null
表示类型为 null
1.6 void
表示类型为 undefined 或 null。
1.6.1 对于变量
一个声明为 void 类型的变量,只能被赋值为 undefined 或 null。这种应用场景较少。
1.6.2 对于函数
void 用于表示函数没有返回值,只是执行某些操作。这是 void 的普遍应用场景。
一个没有显式返回的函数,默认 return undefined。符合 void 类型只能被赋值为 undefined 或 null。
1.7 never
表示永远不存在值。
典型例子:一个只会抛出异常的函数,它的返回值并不存在。
执行函数则抛出错误,连 undefined 都不会返回。
1.8 any
表示可能为任何类型。
典型例子:不确定变量的类型或类型是动态的。
二、引用类型
TS 中的引用类型,除了 JS 中的数组类型、对象类型,还扩展了枚举类型,元祖类型。
2.1 数组类型
表示由某(些)类型组成的数组。有两种写法:方括号表示法和尖括号表示法。
2.1.1 方括号表示法
表示 arr 的类型是由数字组成的数组。
2.1.2 尖括号表示法
使用 Array<元素类型>,这是数组泛型的使用形式,关于泛型后续会展开。
2.2 只读数组
使用 ReadonlyArray<属性类型> 定义只读数组。只读数组只能在数组初始化时定义其值,创建后不能进行修改。
只读数组和常规数组类型 Array<T> 类似。区别在于:常规数组存在修改数组的方法,只读数组不存在修改数组的方法。
2.3 object
表示类型为对象。除了 string、number、bigint、boolean、 undefined、null、symbol 基本类型外的引用类型。
2.4 元组
由定长数组构成,数组中的元素是某(些)类型。
2.5 枚举
使用关键字 enum 定义枚举类型。默认枚举值从 0 开始,可以手动赋值。
三、类型断言
明确告诉 TS 某个值的类型。有两种写法:尖括号写法、as 写法。
尖括号写法
as 写法
注意事项
类型断言和类型转换是有明确区别,不能将它理解成类型转换。
在阐述此注意事项之前,先引入另一个概念:联合类型。
在上述代码中,string | number 表示的就是一个联合类型,意味着 answer 的类型可以为 string 或 number。
下面逐步解释类型断言和类型转换之间的区别。
上述代码中,string | number 联合类型包含 string 类型,所以这两种类型之间存在联系。使用 as 将联合类型断言成更加具体的 string 类型。
上述代码中,将 number 类型断言为 string | number 类型。同样是因为两种类型之间存在联系,所以也允许断言。相当于将一个具体的 number 类型断言成更加广泛的联合类型。
而将 string 类型断言为 number 类型,这是两种不同的类型,没有任何联系,断言会提示错误。
观察上述三个例子,断言发生在两种类型存在联系的情况,它并不是将一种类型转换成另一种类型。
有些脑瓜子灵活的朋友会想到,借助两次断言来进行类似的类型转换。
这是欺骗了 TS 校验,后果只能自己承担。
四、接口
上面提到使用 object 描述对象类型。但是,对于具有复杂结构的对象、函数。上述的 object 类型力有不逮。
上述代码中,即使是空对象,也能通过 object 类型校验,而不会校验对象的结构是否能满足后续使用。
此时,就需要使用接口。在 TS 中,接口是描述值的结构。
上述代码中,就定义了一个接口来描述参数 o,要求它是一个对象,含有 name 属性,且属性值是字符串。
所以,不符合此结构的空对象 {} 就提示错误。
一般来说,使用关键字 interface 来定义接口。
重写上述接口 ——
4.1 固定属性
在接口中,使用 属性名:属性类型 的结构定义固定属性。
如上述代码所示,传入的对象需要具有 name 和 age 两个属性,否则会报错。
4.2 可选属性
在接口中,使用 属性名?:属性类型 的结构定义可选属性。顾名思义,可选属性可以存在,也可以不存在。
4.3 只读属性
在接口中,使用 readonly 属性名:属性类型 的结构定义只读属性。只读属性只能在属性初始化时定义其值,定义后不能进行修改。
4.4 额外检查
TS 会对对象字面量进行额外的属性检查。
在上述代码中,接口 Point 定义了两个可选属性,对象字面量中属性 x 和接口兼容,属性 z 是多余无意义的。
虽然实际的属性比接口定义的多,按照常规理解,这是可以通过类型校验,但事实却相反。
TS 对于对象字面量是会进行额外的属性检查,体现在:
当对象字面量赋值给变量或它直接作为参数传递给函数时,如果对象字面量的属性没有在接口中定义,则会报错。
换句话说,对于对象字面量,当它直接赋值给变量和函数参数时,它的属性不能比接口描述的多。
这里重点是直接赋值,如果像例子中,先将对象字面量赋值给变量,再通过变量传参,这样间接的方法可以绕过额外的检查。
4.5 可索引类型
可索引类型包括字符串索引类型与数字索引类型。
4.5.1 字符串索引类型
字符串索引类型具有字符串索引签名。
4.5.2 数字索引类型
数字索引类型具有数字索引签名
4.5.3 混合索引签名
属性和索引签名可以形成混合索引签名,但是属性需要和索引签名类型匹配。
另外,TS 允许同时使用上述两种签名,但是数字索引返回值的类型,它必须是字符串索引返回值类型的子类型。
因为对于 JS 来说,当使用数字索引时,会将它转换成字符串进行索引。所以它们需要保持一致。
上述代码中,ThreeD 是 TwoD 的子类型,所以接口 PointA 正确。
4.5.4 只读索引签名
可以将索引签名设置为只读,只能在数组初始化时定义其值,创建后不能进行修改。
4.6 函数类型
函数类型具有调用签名。
如上述代码所示,调用签名包括参数列表和返回值类型。
对于函数类型来说,它校验的值当然是函数 ——
如上述代码所示,函数的参数名可以与签名的参数名不同。关键是对应位置的参数类型需要相同。
4.7 类类型
这里的概念有些复杂,如果有良好的 JS 基础,会较易理解。
类的关键字是 Class,由 ES6 开始引入,并逐步完善。本质上 Class 属于语法糖,是基于 prototype 原型链实现的。
而无论是 ES5 或 ES6,属性 age 是在创建的实例上,而方法 getAge() 是在实例的原型链上。
而类本身,它是不存在 age 属性 和 getAge() 方法的。
当然,我们也可以给类本身定义属性和方法。 但给类本身定义的属性和方法并不能通过实例直接访问。
所以,类与实例的属性和方法是割裂的。
TS 将描述类的属性和方法部分称为类的静态部分类型,将描述实例的属性和方法部分称为类的实例部分类型。
在上述代码中,使用关键字 implements 描述类实现了接口。更准确的是:描述 类的实例部分 实现了接口。
实现该接口的类的实例,它是具有 age 属性,getAge() 方法。而类本身(即类的静态部分)具有何种属性与方法,上述代码没有进行定义。
所以,这里所说的类类型,实际上是类(实例的)类型。
关于如何定义类的静态部分的类型,在后续会详细介绍。
4.8 接口继承
接口的可以使用关键字 extends 定义继承。一个接口可以继承多个接口。
4.9 混合类型
一个对象可能混合多种类型。
例如定义一个带版本号的函数——
值得注意,上述使用类型断言,将函数断言为 Fn,即使 fn 在创建时并不存在 version 属性。
4.10 接口继承类
当接口继承类类型时候,表现在继承类的成员和结构,但不包括其实现。
接口继承类的一个场景是,定义一个子类的类类型。
结语
由于文章篇幅问题,全文拆分成上下两篇发布。
本篇主要介绍了 TS 的基本类型,引用类型、类型断言、接口等知识点,了解上述的知识点可以阅读部分 TS 代码。
下篇涉及函数、类、泛型等稍微复杂的知识点。