最近尝试用 ts 和 vue3 rc版本做一个 UI 库,这里记录一下 ts 的学习心得。
下面我们开始吧。
首先进行一个小插曲,记录软件各个版本的差异。
- Alpha:是内部测试版,一般不向外部发布,会有很多Bug.一般只有测试人员使用。
- Beta:也是测试版,这个阶段的版本会一直加入新的功能。在Alpha版之后推出。
- RC:(Release Candidate) 顾名思义么 ! 用在软件上就是候选版本。系统平台上就是发行候选版本。RC版不会再加入新的功能了,主要着重于除错。
- GA:General Availability,正式发布的版本,在国外都是用GA来说明release版本的。
- RTM:(Release to Manufacture)是给工厂大量压片的版本,内容跟正式版是一样的,不过RTM版也有出限制、评估版的。但是和正式版本的主要程序代码都是一样的。
- OEM:是给计算机厂商随着计算机贩卖的,也就是随机版。只能随机器出货,不能零售。只能全新安装,不能从旧有操作系统升级。包装不像零售版精美,通常只有一面CD和说明书(授权书)。
- RVL:号称是正式版,其实RVL根本不是版本的名称。它是中文版/英文版文档破解出来的。
- EVAL:而流通在网络上的EVAL版,与“评估版”类似,功能上和零售版没有区别。
- RTL:Retail(零售版)是真正的正式版,正式上架零售版。
在安装盘的i386文件夹里有一个eula.txt,最后有一行EULAID,就是你的版本。
比如简体中文正式版是EULAID:WX.4_PRO_RTL_CN,繁体中文正式版是WX.4_PRO_RTL_TW。
其中:如果是WX.开头是正式版,WB.开头是测试版。_PRE,代表家庭版;_PRO,代表专业版。
α、β、λ常用来表示软件测试过程中的三个阶段,α是第一阶段,一般只供内部测试使用;β是第二个阶段,已经消除了软件中大部分的不完善之处,但仍有可能还存在缺陷和漏洞,一般只提供给特定的用户群来测试使用;λ是第三个阶段,此时产品已经相当成熟,只需在个别地方再做进一步的优化处理即可上市发行。
下面进入正文
TypeScript 是什么
TypeScript 是一种由微软开发的自由和开源的编程语言。
它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。
下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:
TypeScript 与 JavaScript 的区别
TypeScript | JavaScript |
---|---|
JavaScript 的超集用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页 |
可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
最终被编译成 JavaScript 代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
支持模块、泛型和接口 | 不支持模块,泛型或接口 |
社区的支持仍在增长,而且还不是很大 | 大量的社区支持以及大量文档和解决问题的支持 |
基本数据类型
TS 中继承了所有 JS 中的基本数据类型,并做了一些拓展。
1 | // es 中的基本数据类型 |
基本类型
1 | const b: boolean = true; |
数组类型
1 | const arr1: number[] = [1, 2]; |
元组
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
比如,你可以定义一对值分别为 string和number类型的元组。
元组只允许按照定义时的类型和长度添加元素。
1 | const tuple: [number, string] = [1, 'ts']; |
元组可是使用原生的方法增加元素产生越界。但是无法访问越界元素。
函数类型
在ts中函数不仅可以约定参数类型还可以定义返回值类型。
1 | const add = (x: number, y: number) => x + y; |
以上是一个常见函数。而且此时函数会默认知道自己返回的是 number 类型。这叫类型推断
此时如此赋值就会报错
1 | const text: string = add(1, 3); // error |
对象类型
1 | let obj: object = {x: 1, y: 2}; |
undefined & null
1 | let un: undefined = undefined; |
以上代码可以看出 undefined 和 null 是所有类型的子类型。
如果出现其他类型复制 undefined 失败的情况。可以将 tsconfig 中的 strictNullChecks 设置为 false。
void
void 表示:一个表达式的返回值是 undefined
1 | let fun(x: number) => void |
此时 testFun 的返回值为 undefined。
1 | let fun = (x) => x * 3 // error |
any
任意类型
never
never 代表永远不会有返回值的类型。
注意和 void 的区分。
- void:有返回值,返回 undefined
- never:没有返回值
1
2
3
4
5
6const fun = () => {
let x = 1;
while(true) {
x += 1;
}
}
枚举
在 ts 中枚举类型细分下来有三种
数字枚举
字符串枚举
异构枚举
数字枚举
以下代码声明了一个数字类型的枚举。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21enum BanGong {
cup,
pen,
computer
}
BanGong[0] // cup
BanGong.pen // 1
// 如果你对索引进行一些干预则会有不同的效果
enum BanGongAgain {
cup = 1,
pen,
computer = 5,
banana,
}
BanGongAgain.pen // 2
BanGongAgain[0] // ?
BanGongAgain.banana // ?ts 是如何实现数字枚举的?
1
2
3
4
5
6var BanGong;
(function (BanGong) {
BanGong[BanGong["cup"] = 0] = "cup";
BanGong[BanGong["pen"] = 1] = "pen";
BanGong[BanGong["computer"] = 2] = "computer";
})(BanGong || (BanGong = {}));这种方法学名叫做反向映射。
字符串枚举
1
2
3
4
5
6
7enum Message {
success = '成功',
fail = '失败'
}
Message.success // 成功
Message['成功'] // undefined
以上代码可以看出,字符串枚举类型是不支持反向映射的。
异构枚举
异构枚举:数字枚举和字符串枚举的混合装。1
2
3
4enum Answer {
N,
Y = 'Yes'
}枚举的其他特性
1
2
3
4
5
6
7
8enum TestEnum {
// 常量型枚举:在编译时就已经得出结果
a,
b = 1 + 2,
// 计算型枚举:在运行时才有结果。定义在此后的枚举值一定要有初始值
d = 'abc'.length(),
e, // error
}常量型枚举在编译时候会被移除。
但我们不需要对象。仅仅需要对象值的时候就可以使用常量枚举以减少代码编译后的体积。
接口
接口分为以下类型
对象类型接口
函数类型接口
- 对象类型接口和鸭式辨形法
来看一个最基本的接口用法。以上代码中 res 可能是后端返回的一段数据,也可能是你自己写的一段 JSON。1
2
3
4
5
6
7
8
9
10interface List = {
id: number;
name: string;
}
function handleList(res: List[]) {
res.forEach(listItem => {
console.log(listItem.name)
})
}
1 | const res = [ |
以上代码中,数据多了 age 属性。
但是 ts 依然没有报错。这个就叫做鸭式辨形法。
只要 res 的元素中包含了 List 的必要属性,那么 ts 就认为他是一段正确的数据。
如果变成这样那么就会有问题了。
1 | handleList( |
因为以上代码中,没有 name 这个必要的属性所以报错了。
- 函数类型接口
声明函数接口的方式
变量法:
1 | let add: (x: number, y: number) => number; |
接口法:
1 | interface Add { |
类型别名:
1 | type Add = (x: number, y: number) => number; |
注意:以上三种方式只是对函数进行了定义,而没有实现。
实现函数
1 | let add: Add = (a, b) => a + b; |
混合类型接口
混合类型接口意思是:一个接口既可以定义一个函数,也可以像对象一样拥有属性和方法。
1 | interface MixinType { |
其实以上代码定义完成后,编译器依然会报错。所以你可能需要一个类型断言。
1 | let mixinType: MixinType = (() => {}) as MixinType; |
- 接口的继承
类的继承,可以抽离公共的方法也可以将多个接口合并成一个方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface Human {
name: string;
eat(): void;
}
interface Man EXTENDS Human {
beard: string
}
interface Tom extedns Man {}
let tom: Tom {
name: '',
beard: ''
}
泛型
- 函数泛型
我们可以利用函数重载来实现一个类型灵活的函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14function log(value: string): string {
console.log(value);
return value;
}
function log(value: number[]): number {
console.log(value);
return value;
}
function log(value: any): any {
console.log(value);
return value;
}
或者也可使用联合类型。
1 | function log(value: string | number[]): string | number { |
但是以上的方法依然不够灵活。现在我们可以使用泛型。
泛型允许你在调用的时候才传入真正的类型,从而实现最大的灵活性。
1 | function log<T>(value: T): T { |
泛型同时也支持多个参数
1 | function log<T, U>(one: T, Two: U) { |
泛型也可以用来约束接口成员
1 | interface Log<T> { |
- 泛型类
先来看一个最简单的泛型类的实现。1
2
3
4
5
6
7
8
9
10
11
12class Log<T> {
run(value: T) {
console.log(value);
return value;
}
}
let log1 = new Log<number>();
log1.run(2);
let log2 = new Log();
log2.run('ttt');
log2.run({ test: 'test' })
注意:泛型类不能用于静态成员,以下代码是会报错的。
1 | class Log<T> { |
泛型约束
先来看一段代码
1 | function log<T>(value: T): T { |
此时我们就要用到类型约束了。
1 | interface Length { |
复制代码此时编译器就不会报错了。
但是同时 T 也不可以再传任意类型的参数进来。
参数必须是有 length 属性的参数。此时我们就说 T 受到了类型约束。
高级类型
高阶类型大致可以分为以下几种
交叉类型
联合类型
索引类型
映射类型
条件类型
- 交叉类型
将多个类型合并为一个类型。新的类型将具有所有类型的特性。
所以交叉类型特别适合对象混入的场景。
1 | interface DogType { |
可以看到上面的 pet宠物 就是 狗 和 猫的交叉类型。
注意:交叉类型并不是取所有类型的交集,而是取所有类型的并集。
如果我们在交叉类型中定义了同样的方法会发生什么呢?看看以下代码。
1 | interface DogType { |
接口属性分为两种,如果是基本类型比如:
1 | interface A { |
此时 foo 的属性 a 应该是 never 即 number & string 的类型。
但是 ts 没有任何值可以赋值给 never 类型,所以无论怎么赋值都会失败。
如果属性是函数那么情况就有所不同。
1 | interface A { |
1 | interface aa { |
我们看到在代码中没有报错。
在交叉类型中,函数 f 实际发生了函数重载。
为了同时兼容 A 和 B 中的定义,参数要选择最少的。参数的返回值要取 number & string 即 never 或者 any。
但是实际上我们应该避免以上这种写法。
- 联合类型
当声明的类型并不确定,可能是多个类型中的中的一个,叫做联合类型。
联合类型也可以分为不同的种类。
基本类型的联合类型
1 | let a: number | string = 1; |
对象的联合类型
此处我们要用到交叉类型中的两个接口
1 | class Dog implements DogType { |
可以看到当我们输入 pet. 的时候编译器只显示了 eat,而如果我们访问其他的属性则会报错。
所以我们可以得知。联合类型听起来是 Dog 和 Cat 的并集,实际上是其交集。正常情况下只能访问到两个类型的共有属性。
可区分的联合类型
利用联合类型的共有属性。我们就可以建立一系列的分支区块。
1 | interface Square { |
以上代码中 area 是一个计算形状面积的函数。
表面上看起来没有什么问题,但是如果我们此时扩展一个类型就会发现有点问题。
我们增加了一个圆形。
调用了 area 函数进行计算并打印结果。
此时打印出了 undefined。
很明显此时的 switch 语句没有覆盖到所有的情况,但是是我们的 TS 并没有报错。怎么办?
此时 TS 就会检查 switch 是否覆盖到了所有的情况。
2.利用 never 类型
default 函数的作用就是检查 s 是不是 never 类型。
如果 s 是 never 类型说明 case 分支已经覆盖了所有的情况 default 函数永远都不会被执行。
如果 s 不是 never 类型就说明 case 分支有遗漏就会报错。
索引类型
先来看一个场景:我们需要在一个对象中抽取一些属性值生成一个数组,以下是代码。
1 | let myObj = { |
我们访问了两个 myObj 中不存在的属性,但是 TS 没有报错。如果才能让 TS 帮助我们对类型进行一些检查呢?此时就需要用到索引类型。
请先了解如下概念
1.索引类型的查询操作符
1 | // keyof T:表示类型 T 的所有公共属性的字面量的联合类型 |
2.索引访问操作符
1 | interface Obj { |
3.泛型约束
1 | // T extends U 表示泛型变量可以继承某个类型,从而得到一些属性 |
有了以上三个概念我们就可以改造 getValues 函数了。
1 | let myObj = { |
由此可以看到,索引类型可以实现对【对象属性】的查询和访问。
然后配合泛型约束我们就可以建立【对象】【对象属性】【属性值】之间的约束关系。
- 映射类型
通过映射类型,我们可以通过一些旧的类型生成新的类型。来看看代码我们来看看有什么效果1
2
3
4
5
6
7
8
9interface Obj {
a: string;
b: number;
c: boolean;
}
type ReadOnly<T> = {
readonly [P in keyof T]: T[P]
}
type ReadOnlyObj = ReadOnly<Ojb>
此时 ReadOnlyObj 和 Obj 中的属性相同但是全都变成了只读属性。
我们来看看 type ReadOnly 做了什么。
1 | // readonly 是一个可索引的泛型接口 |
其实 TS 已经内置了很多的映射类型,包括以上的 ReadOnly。
可以查看 node_modules/typescript/lib/lib.es5.d.ts
中的内容获得更多的信息。
以下列举了几个可能常用的映射类型。
1 | // 只读:将 Obj 的所有属性变为只读 |
以上三种类型官方统一称为同态,也就是不会引入新的属性
1 | type RecordObj = Record<'x' | 'y', Obj> |
Record 就是一个非同态类型
ts 的其他特性
- ts的命名空间
在 js 中命名空间可以避免全局污染,但是在 es6 引入模块化概念后命名空间就比较少被用到了。
在 ts 中依然实现了这个特性,虽然在模块系统中我们不用考虑全局污染的问题,但是如果使用了全局类库,命名空间依然是比较好的解决方案。
命名空间的基本使用
命名空间使用 namespace 关键字。
1 | /* |
命名空间的拆分
命名空间的拆分是可以跨文件的。
如果上一段代码是 circle.ts 那么我们在另一个文件 square.ts 中声明一个同名的命名空间,只需要用三斜线指令指定引用就可以正常使用 circle.ts 中的方法了。
1 | // <reference path="circle.ts" /> |
命名空间是如何实现的
我们直接来看看代码的编译结果就可以了
1 | var Shape; |
Shape 被编译成了一个立即执行的函数,函数形成了一个闭包,pi 是一个私有成员,而 export 出的 circle 则被挂载到了一个全局变量上。
命名空间的别名
我们可以给命名空间中的方法起一个别名方便我们引用。
1 | import circle = Shape.circle; |
注意此处的 import 和模块化引用没有任何关系。
- ts的声明合并
声明合并是 ts 中的独特概念。
概念是:编译器会把程序多个地方具有相同命名的声明合并为一个声明。
好处就是当你声明了多个同名接口,在实现的时候你将会同时对多个接口有感知能力,可以避免对接口成员的遗漏。
接口的声明合并
先来看以下代码
1 | interface A { |
以上代码中,接口 A 并没有出现覆盖的情况而是被合并到了一起。如果该代码是一个全局性的文件,两个 interface A 甚至可以不在同一个文件中也可以实现声明合并。
注意,接口的声明合并中如果重复声明非函数的方法,要求类型相同
1 | interface A { |
对于重复声明的函数方法,每个方法都会变成函数重载
1 | interface A { |
以上代码中对于 fun 就是实现了函数的重载。
在 ts 中函数的重载是由解析顺序的。
在正常情况下会按照从最早的声明开始寻找。
但是在接口合并中规则稍有变化。
同一接口中从早声明到晚声明,不同接口中从晚声明到早声明,如果函数的参数是一个字符串字面量的话该声明会被提升到最顶端。
按照以上的规则在刚在的示例代码中,函数重载的查找规则是:
1 | interface A { |
工程篇
- tsconfig 配置
本节列举了一些ts常见的配置
文件选项
1 | { |
编译相关
编译相关的选项有100多个。一下列举常用的选项。
1 | { |
部分内容参考自ts学习指南
另外风格,更详细的demo,可参考该篇文章
- 本文作者: Jambo
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!