初识

TypeScript 是由巨硬开发的 JS 的超集,通过添加可选的静态类型和基于类的面向对象编程思想,使得代码更易于理解、维护和重构。此外,TS 还支持其他高级语言功能,如接口、泛型、命名空间和装饰器等

规范:TS 提供了更好的类型检查和代码提示工具,并且可以使用第三方库中的类型声明文件进行代码补全和类型检查

开发:TS 为现代Web应用程序和大型项目提供了一个强大的编程环境,可以帮助开发人员减少错误、提高开发效率和代码质量

运行:TS 可以转换成 JS 并在任何支持 JS 的环境中运行,包括浏览器、Node.js和移动应用程序等

安装:npm install typescript -g 会在全局暴露一个 tsc 命令

tsc -init 初始化,创建 tsconfig.json

tsc -w 监视所有 TS 文件变化,实时编译成 JS

npm install ts-node -g 安装 ts-node 更方便地运行 TS

配置文件

tsconfig.json 是 TS 的配置文件

可以定义哪些文件需要包含在编译过程中、使用哪个ECMAScript目标版本、生成哪种模块系统、是否开启严格模式等等

常用配置项:

  1. target 设置编译后的 JavaScript 代码使用的 ECMAScript 版本,默认为 ES3,可设置为 ES5、ES6/ES2015、ES7 等。
  2. strict 开启所有严格类型检查选项,包括noImplicitAny,noImplicitThis,alwaysStrict等等。如果希望 TypeScript 给出尽可能多的类型检查错误提示,可以将此选项设置为 true。
  3. module 指定生成的模块规范类型,可选值有 commonjs、amd、system 和 es2015。
  4. lib 指定编译过程中需要引入的库文件,默认情况下只包含 DOM 和 ES 标准库,可以通过指定其他库来获得更好的类型检查支持。
  5. moduleResolution 指定模块解析策略,可选值有 classic、node、和 yarn pnp。
  6. esModuleInterop 简化导入cjs模块功能
  7. outDir 输出目录,指定编译后的 JavaScript 代码所在的目录。
  8. rootDir 项目根目录
  9. allowJs 允许编译器编译 .js 文件(通常是为了容易迁移现有的 JavaScript 项目到 TypeScript 项目)
  10. sourceMap 生成 sourcemap 文件,方便调试 TypeScript 代码时能够正确地映射回原始的 TypeScript 代码。
  11. noImplicitAny 当 TypeScript 无法推导出变量的类型时,给出一个错误提醒
  12. noImplicitThis 禁止 this 关键字的隐式 any 类型
  13. strictNullChecks 严格模式下对 null 和 undefined 的检查。

TS检查相关注释

(1) // @ts-nocheck 加到文件首行,当前文件不需要 ts 校验
(2) // @ts-check 加到文件首行,对当前文件进行 ts 校验
(3) // @ts-ignore 忽略下一行代码的 ts 校验
(4) // eslint-disable-next-line 忽略下一行代码的 eslint 校验
(5) /* eslint-disable */ eslint忽略

类型标注

通过 <数据>:<类型> 对变量、函数返回值、函数参数等数据进行类型的标注、限制

思想:定义任何东西的时候要注明类型,调用任何东西的时候要检查类型。

可以标注的类型:
(1) 基础类型:Boolean、Number、String、null、undefined 以及 ES6 的 Symbol 和 ES10 的 BigInt。
(2) 空值:Void
(3) 顶级类型:任意类型 Any 和 不知道的类型 Unknown
(4) Object、object 和 {}
(5) 接口和对象类型:interface定义
(6) 数组类型
(7) 函数类型

基本类型

1、字符串类型:string

1
2
3
let str: string = "chuckle";
str = `1+1=${1+1}`; // 可以使用模板字符串
str = "1+1=" + "2"; // 可以拼接

2、数字类型:number

1
2
3
4
5
let num: number = 1
num = NaN
num = Infinity
num = 0xf00d // 十六进制
num = 0b1010 // 二进制

3、布尔类型:boolean

注意:new Boolean() 返回的是一个 Boolean 对象,而不是布尔值

1
2
let bool: boolean = true
bool = Boolean(0) // false

4、Null 和 undefined 类型

undefined 和 null 是所有类型的子类型,在非严格模式下可以赋给其它任何类型的变量

严格模式下:
(1) undefined 和 null 不能赋给其它类型的变量,但 undefined 可以赋给 void 类型(一般也用不到)
(2) null 和 undefined 类型也不能相互赋值

1
2
3
4
5
6
let a: null = null
let b: undefined = undefined
a = b // 严格模式下不允许
let str: string = a // 严格模式下不允许
let c: void = null // 严格模式下不允许
c = undefined // 始终允许

空值void

JS 中没有空值的概念,TS 中可以使用 void 表示函数无返回值

注意:
(1) 不能将 void 赋给除 any 外其它类型的变量。
(2) 使用 any 类型的变量接收函数返回的空值,打印 undefined
(3) Boolean(void) 值是 false

1
2
3
4
5
6
function fun(): void{}
let a: any = fun();
console.log(a); // undefined
a = Boolean(fun());
console.log(a); // false
let b: string = fun() // 不允许

any和unknown

any 和 unknown 是 TS 中的顶级类型,可以包含所有类型的数据

如果所有变量都是 any 类型,那就是 AnyScript,写起来和 JS 没什么区别,最好不要这么做

1
2
3
4
let a: any = 1;
a = "qx"
a = true
a = null

any 与 unknown 的区别:
any 可以赋给其它类型,而 unknown 不能赋值给除 any 和 unknown 以外的其它类型

1
2
3
4
5
let a: any = 1
let b: unknown = 2
let num: number = a // any可以赋给其它类型
a = b // unknown可以赋给any
num = b // unknown不能赋给其它类型

unknown 类型不能读任何元素、属性,也不能调用任何方法,所以 unknown 比 any 安全

1
2
3
4
5
let u: unknown = { a: 1 }
console.log(u); // 可以
console.log(u.a); // 错误
u = ()=>{ console.log('@@') }
u() // 错误

Object、object和{}

Object、object 和 {} 虽然能保存的数据类型不同,但都和 unknown 类型一样,不能读任何元素、属性,也不能调用任何方法

1、Object 类型是所有 Object 类的实例的类型,字面量 {} 代表的也是 Object

由于原型链顶层就是 Object,所有基本数据类型和引用类型最终都指向 Object,所以他也包含所有类型

1
2
3
4
5
let obj: Object = 1;
obj = "chuckle"
console.log(obj); // chuckle
obj = { a: 1 }
console.log(obj.a); // 报错:类型Object上不存在属性a

2、object 代表所有非值类型的类型(数组、对象、函数)等,常用于泛型约束

1
2
3
4
5
6
let o: object = ()=>{ console.log('@@') }
o() // 此表达式不可调用。类型{}没有调用签名
o = ['1','2']
console.log(o[1]) // 错误
o = { a: 1 }
console.log(o) // 报错:类型object上不存在属性a

接口和对象类型

在 TypeScript 中,使用接口(Interfaces)来定义对象的类型。

即使用 interface 来定义一种对对象的约束

接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象外,也常用于对象的形状的描述。

1
2
3
4
5
6
7
8
9
10
11
// 定义一个对象
interface Person{
name: String,
age: number,
}
// 必须按照定义的形状来声明
let person: Person = {
name: "chuckle",
age: 20
}

1、重名的接口会合并,共同起作用

1
2
3
4
5
6
7
8
9
10
11
interface Person{
name: String,
}
interface Person{
age: number,
}
let person: Person = {
name: "chuckle",
age: 20
}

2、任意属性 [propName: string] (索引签名)

声明的对象中可以有任意个数所定义的类型(或其子类型)的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person{
name: String,
age: number,
[propName: string]: any
}
let person: Person = {
name: "chuckle",
age: 20,
// 接下来可以有任意个数any类型的属性
a: 1,
b: "qx",
c(){
console.log(this.a)
}
}

3、readonly 让属性只读

1
2
3
4
interface Person{
readonly name: String,
age: number,
}

4、定义对象中的函数

1
2
3
interface Person{
fun: (value:number)=>number
}

6、接口继承 extends

1
2
3
4
5
6
7
8
9
10
11
interface A{
name: String,
}
interface B extends A{
age: number,
}
let b: B = {
name: "chuckle",
age: 20,
}

数组类型

使用 :<元素类型>[] 来定义一个数组

1
let arr: number[] // 定义数值类型的数组

使用泛型的方式定义

1
let arr: Array<number>

定义二维数组

1
2
let arr: number[][]
let arr: Array<Array<number>>

定义包含多种类型的数组

1
let arr: (number|string)[] = [1, 2, '3', 4]

元组

元组:赋值时,元素的类型、位置、个数必须一一对应

1
let arr: [number,string] = [1,'qx']

允许起名加上可选修饰符,对于越界的元素他的类型被限制为联合类型,也就是可以push已有的元素类型

1
2
3
4
const arr: [x: number, y?: boolean] = [1, true]
arr.push(1) // 允许push已有的元素类型
arr.push("1") // 类型“"1"”的参数不能赋给类型“number | boolean | undefined”的参数。
console.log(arr)

可以用于表格数据的约束

1
2
3
4
5
let excel: [name: string, sex: string, age: number][] = [
['张三', '男', 18],
['张三', '男', 18],
['张三', '男', 18]
]

函数类型与定义

定义一个函数类型变量

1
2
let fun: Function
fun = ()=>{}

定义函数的参数和返回值

1
2
3
4
function fun(a:number, b:number):number{
return a+b;
}
console.log(fun(1,1))

使用 interface 定义函数类型(函数也是一种对象)

1
2
3
4
5
6
7
8
9
10
interface Fn {
// 只有一个string类型的参数,返回值是数值类型
(name: string): number
}
let fun: Fn = (name)=>{
console.log(name)
return 1
}
fun("chuckle")

TS 可以定义函数中的 this 类型,用于增强补全,必须写在函数的第一个参数上,不作为真正的参数定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Obj{
name: string
// TS可以定义this的类型(一般用于增强补全),必须在第一个参数上定义
add: (this:Obj, num:number)=>void
}
let obj:Obj = {
name: "chuckle",
add(num:number){
console.log(num);
console.log(this.name);
// 此时this.就会有补全
}
}
obj.add(1);

?可选符

使用 ?: 定义函数参数或对象属性作为可选项

1
2
3
4
5
6
7
8
9
interface Person{
name: String,
age?: number, // 年龄可选
}
// 第二个b参数可选
function fun(a:number, b?:number):number{
return b ? a+b : a;
}

联合类型

使用 <类型1>|<类型2> 标注多个类型

1
2
3
4
// 函数可传入两种参数
function fun(a: number | string) { }
// 可保存两种类型的变量
let a: number | string

交叉类型

<类型1>&<类型2> 需同时满足两种类型,通常配合接口使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Student {
name: string
id: number
}
interface Person {
name: string
age: number
}
let p: Student & Person = {
name: "chuckle",
id: 1,
age: 20
}

类型断言

值 as 类型<类型>值 欺骗 TypeScript 编译器,但无法避免运行时的错误

1
2
3
4
5
6
function fun(a: number | string){
// console.log(a.length); // 数值类型没.length方法,会报错
console.log((<string>a).length); // 断言a一定是string类型
}
fun(123) // undefined
fun('123') // 3

TS 直接在 window 上添加属性是不允许的,但可以将 window 断言为 any 类型,就可以添加属性了

1
2
3
window.a = 1; // 不可以
(<any>window).a = 1; // 断言成any就可以了
console.log((<any>window).a);

内置对象

JS 中有很多内置对象,它们可以直接在 TS 中当做定义好了的类型来使用

1、ECMAScript 的内置对象:内置对象名就是类型名

1
2
3
4
5
6
7
8
9
10
11
12
13
let b: Boolean = new Boolean(1)
console.log(b)
let n: Number = new Number(true)
console.log(n)
let s: String = new String('chuckle')
console.log(s)
let d: Date = new Date()
console.log(d)
let r: RegExp = /^123/
console.log(r)
let e: Error = new Error("error")
console.log(e)

2、DOM 和 BOM 的内置对象:
HTML<标签名>Element 某个内置标签的类型,input、span 等
HTMLElement 语义化的标签名,footer、header,以及自定义标签名
Element 任何标签元素的类型
NodeList 任何元素集合的类型
NodeListOf<其它类型> 指定元素集合的类型

由于元素可能获取不到,要加上 null 组成联合类型

1
2
3
4
5
6
let div1: HTMLInputElement | null = document.querySelector('input');
let div2: HTMLDivElement | null = document.querySelector('div');
let div3: NodeListOf<HTMLElement> | null = document.querySelectorAll('.content');
let local: string | null = localStorage.getItem('token')
let lct: Location = location
let pms: Promise<number> = new Promise((r)=>r(1))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//dom元素的映射表
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
"area": HTMLAreaElement;
"article": HTMLElement;
"aside": HTMLElement;
"audio": HTMLAudioElement;
"b": HTMLElement;
"base": HTMLBaseElement;
"bdi": HTMLElement;
"bdo": HTMLElement;
"blockquote": HTMLQuoteElement;
"body": HTMLBodyElement;
"br": HTMLBRElement;
"button": HTMLButtonElement;
"canvas": HTMLCanvasElement;
"caption": HTMLTableCaptionElement;
"cite": HTMLElement;
"code": HTMLElement;
"col": HTMLTableColElement;
"colgroup": HTMLTableColElement;
"data": HTMLDataElement;
"datalist": HTMLDataListElement;
"dd": HTMLElement;
"del": HTMLModElement;
"details": HTMLDetailsElement;
"dfn": HTMLElement;
"dialog": HTMLDialogElement;
"dir": HTMLDirectoryElement;
"div": HTMLDivElement;
"dl": HTMLDListElement;
"dt": HTMLElement;
"em": HTMLElement;
"embed": HTMLEmbedElement;
"fieldset": HTMLFieldSetElement;
"figcaption": HTMLElement;
"figure": HTMLElement;
"font": HTMLFontElement;
"footer": HTMLElement;
"form": HTMLFormElement;
"frame": HTMLFrameElement;
"frameset": HTMLFrameSetElement;
"h1": HTMLHeadingElement;
"h2": HTMLHeadingElement;
"h3": HTMLHeadingElement;
"h4": HTMLHeadingElement;
"h5": HTMLHeadingElement;
"h6": HTMLHeadingElement;
"head": HTMLHeadElement;
"header": HTMLElement;
"hgroup": HTMLElement;
"hr": HTMLHRElement;
"html": HTMLHtmlElement;
"i": HTMLElement;
"iframe": HTMLIFrameElement;
"img": HTMLImageElement;
"input": HTMLInputElement;
"ins": HTMLModElement;
"kbd": HTMLElement;
"label": HTMLLabelElement;
"legend": HTMLLegendElement;
"li": HTMLLIElement;
"link": HTMLLinkElement;
"main": HTMLElement;
"map": HTMLMapElement;
"mark": HTMLElement;
"marquee": HTMLMarqueeElement;
"menu": HTMLMenuElement;
"meta": HTMLMetaElement;
"meter": HTMLMeterElement;
"nav": HTMLElement;
"noscript": HTMLElement;
"object": HTMLObjectElement;
"ol": HTMLOListElement;
"optgroup": HTMLOptGroupElement;
"option": HTMLOptionElement;
"output": HTMLOutputElement;
"p": HTMLParagraphElement;
"param": HTMLParamElement;
"picture": HTMLPictureElement;
"pre": HTMLPreElement;
"progress": HTMLProgressElement;
"q": HTMLQuoteElement;
"rp": HTMLElement;
"rt": HTMLElement;
"ruby": HTMLElement;
"s": HTMLElement;
"samp": HTMLElement;
"script": HTMLScriptElement;
"section": HTMLElement;
"select": HTMLSelectElement;
"slot": HTMLSlotElement;
"small": HTMLElement;
"source": HTMLSourceElement;
"span": HTMLSpanElement;
"strong": HTMLElement;
"style": HTMLStyleElement;
"sub": HTMLElement;
"summary": HTMLElement;
"sup": HTMLElement;
"table": HTMLTableElement;
"tbody": HTMLTableSectionElement;
"td": HTMLTableDataCellElement;
"template": HTMLTemplateElement;
"textarea": HTMLTextAreaElement;
"tfoot": HTMLTableSectionElement;
"th": HTMLTableHeaderCellElement;
"thead": HTMLTableSectionElement;
"time": HTMLTimeElement;
"title": HTMLTitleElement;
"tr": HTMLTableRowElement;
"track": HTMLTrackElement;
"u": HTMLElement;
"ul": HTMLUListElement;
"var": HTMLElement;
"video": HTMLVideoElement;
"wbr": HTMLElement;
}

代码雨

1
2
3
4
5
6
7
8
*{
padding: 0;
margin: 0;
overflow: hidden;
}
body{
background-color: #000;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import './index.css'

let canvas: HTMLCanvasElement = document.querySelector('#canvas');
let ctx = canvas.getContext('2d');
canvas.width = screen.availWidth;
canvas.height = screen.availHeight;

let str: string[] = 'qcqxchuckle0101010101'.split('');
let arr = Array(Math.ceil(canvas.width / 10)).fill(0);

const rain = () => {
// 不断的在绘制矩形,没有清除上一次画布,矩形是叠加起来的半透明的,所以背景越来越黑,文字越来越谈,感觉像渐变
ctx.fillStyle = 'rgba(0,0,0,0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#0f0';
arr.forEach((item, index) => {
if (Math.random() > 0.2) { // 控制字符显示的概率
const text = str[Math.floor(Math.random() * str.length)]; // 随机获取一个文字
ctx.fillText(text, index * 10, item); // 间隔10px绘制文字
}
// 文字到底部或者大于一个随机数则重置到顶部
arr[index] = item > canvas.height || item > Math.random() * canvas.height * (canvas.height > 1000 ? 20 : 10) ? 0 : item + 10
});
};

let stv = setInterval(rain, 40);
window.addEventListener('resize', () => {
clearInterval(stv);
canvas.width = screen.availWidth;
canvas.height = screen.availHeight;
stv = setInterval(rain, 40);
});

Class

Class类是ES6新特性,详见:ES6-Class类

在TS中使用interface定义类,当然interface还能定义对象,因为对象是类的实例,类是对象的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用来定义对象的类型
interface Options {
el: string | HTMLElement
}
// 用来约束Class
interface VueCls {
options: Options
init(): void
}
// implements关键字定义类
class Vue implements VueCls {
options: Options
constructor(options: Options) {
this.options = options
this.init()
}
init(): void {
console.log(this.options) // { el: '#app' }
}
}
const vue = new Vue({
el: '#app'
});

一个类可以由多个interface进行约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface VueCls1 {
options: Options
}
interface VueCls2 {
init(): void
}
// implements关键字定义类
class Vue implements VueCls1, VueCls2 {
options: Options
constructor(options: Options) {
this.options = options
this.init()
}
init(): void {
console.log(this.options) // { el: '#app' }
}
}

继承

Class 通过 extends 关键字实现继承,详见:ES6-类的继承

一个简化版的虚拟DOM案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 用来定义对象的类型
interface Options {
el: string | HTMLElement
}

// 用来约束Class
interface VueCls {
options: Options
init(): void
}

// 定义节点的数据结构
interface Vnode {
tag: string // 标签名
text?: string // 文本内容
children?: Vnode[] // 子节点
}

// 简化版虚拟Dom
class Dom {
// 创建节点
createElement(tag: string) {
return document.createElement(tag);
}
// 填充文本
setText(el: HTMLElement, value: string) {
el.textContent = value;
}
// 渲染
render(data: Vnode) {
// 创建当前节点
const root = this.createElement(data.tag);
// 如果有文本则填充
if (data.text) {
this.setText(root, data.text)
}
// 如果该有子节点,则递归渲染
if (data.children && Array.isArray(data.children)) {
data.children.forEach(child => {
// 渲染并获取子节点
const e = this.render(child)
// 将子节点添加给父节点
root.appendChild(e);
})
}
// 返回当前节点
return root;
}
}

// 虚拟dom数据
const data: Vnode = {
tag: 'div',
text: "这是一个div标签",
children: [
{
tag: 'p',
text: "这是一个p标签"
},
{
tag: 'div',
children: [
{
tag: 'p',
text: "这是一个p标签"
},
{
tag: 'span',
text: "这是一个span标签"
},
]
},
],
}

// extends实现继承,需要写在implements之前
class Vue extends Dom implements VueCls {
options: Options
constructor(options: Options) {
super();
this.options = options
this.init()
}
init(): void {
// 调用父类的方法
const dom = this.render(data)
const app = typeof this.options.el === "string" ? document.querySelector(this.options.el) : this.options.el
// app存在则将Dom挂载到页面上
app?.appendChild(dom)
}
}

// 创建实例
const vue = new Vue({
el: '#app'
});

渲染结果:

1
2
3
4
5
6
7
8
9
<div id="app">
<div>这是一个div标签
<p>这是一个p标签</p>
<div>
<p>这是一个p标签</p>
<span>这是一个span标签</span>
</div>
</div>
</div>

访问修饰符

访问修饰符是TS提供的特性,总共有三个:public private protected,用于控制对类内部成员的访问权限

  1. public 默认,实例和子类都能访问
  2. private 私有的,只能在类内部访问,实例和子类都不能访问
  3. protected 类和子类能访问,实例不能访问

一个特殊的:readonly 只能访问,不能修改(可以在constructor中初始化)

对上面的虚拟dom案例进行修饰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 用来定义对象的类型
interface Options {
el: string | HTMLElement
}

// 用来约束Class
interface VueCls {
options: Options
init(): void
}

// 定义节点的数据结构
interface Vnode {
tag: string // 标签名
text?: string // 文本内容
children?: Vnode[] // 子节点
}

// 简化版虚拟Dom
class Dom {
// 创建节点
private createElement(tag: string) {
/*****/
}
// 填充文本
private setText(el: HTMLElement, value: string) {
/*****/
}
// 渲染
protected render(data: Vnode) {
/*****/
}
}

// implements关键字定义类
class Vue extends Dom implements VueCls {
readonly options: Options
constructor(options: Options) {
super();
/*****/
}
init(): void {
/*****/
}
}

注意:接口中不能出现 public, protected, private 修饰符,因为默认只能规定类中的 public 属性和方法,也就是说接口中定义的成员都是 public 的

1
2
3
4
5
6
7
8
9
10
11
interface VueCls {
/*****/
init(): void
}
class Vue extends Dom implements VueCls {
/*****/
// 报错:属性“init”在类型“Vue”中是私有属性,但在类型“VueCls”中不是。
private init(): void {
/*****/
}
}

抽象类

abstract 关键字定义抽象类和抽象方法,抽象类无法实例化,抽象方法只能描述

作用:用于顶层设计,派生类实现功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class Person {
name: string
constructor(name: string) {
this.name = name
}
abstract setName(name: string): void
}

class Student extends Person {
// 子类必须实现抽象父类的抽象方法
setName(name: string): void {
this.name = name
}
}

const stu = new Student("chuckle")
console.log(stu.name) // chuckle
stu.setName("qcqx")
console.log(stu.name) // qcqx

枚举类型

enum 定义枚举类型,枚举成员只能是数字或字符串,且readonly

枚举成员也可以被当做一个类型,可以指定某些变量的值必须是枚举成员的值

聊聊TypeScript中枚举对象(Enum)

1、数字枚举
默认从0开始枚举,可以反向映射

1
2
3
4
5
6
7
8
9
enum Color {
red,
green,
blue
}
console.log(Color)
// { '0': 'red', '1': 'green', '2': 'blue', red: 0, green: 1, blue: 2 }
console.log(Color[0]) // red
console.log(Color.red) // 0

可以指定初值

1
2
3
4
5
6
7
8
9
enum Color {
red = 1,
green,
blue
}
console.log(Color)
// { '1': 'red', '2': 'green', '3': 'blue', red: 1, green: 2, blue: 3 }
console.log(Color[0]) // undefined
console.log(Color.red) // 1

2、字符串枚举
每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化

1
2
3
4
5
6
7
8
enum Color {
red = 'red',
green = 'green',
blue = 'blue'
}
console.log(Color)
// { red: 'red', green: 'green', blue: 'blue' }
console.log(Color.red) // red

3、异构枚举
枚举可以混合字符串和数字成员

1
2
3
4
5
6
7
enum Color {
red = 'red',
green = 'green',
blue = 3
}
console.log(Color)
// { '3': 'blue', red: 'red', green: 'green', blue: 3 }

4、接口枚举
用于约束成员值的范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Color {
red = 'red',
green = 'green',
blue = 3
}
interface A {
red: Color.red;
blue: Color.blue;
}
let obj: A = {
// red: 'red', // 不能将类型“"red"”分配给类型“Color.red”。
red: Color.red,
// blue: 2 // 不能将类型“2”分配给类型“Color.blue”。
blue: 3
}

4、const枚举
直接将枚举编译为常量,而普通声明的枚举编译完后是个对象,可以避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问

注意:const枚举不能反向映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const enum Types{
No = "No",
Yes = 1,
}
console.log(Types.No)
console.log(Types.Yes)
// 编译后
console.log("No" /* Types.No */);
console.log(1 /* Types.Yes */);
// 非const编译后
var Types;
(function (Types) {
Types["No"] = "No";
Types[Types["Yes"] = 1] = "Yes";
})(Types || (Types = {}));
console.log(Types.No);
console.log(Types.Yes);

类型推论

TS会在没有明确的指定类型的时候推断出一个类型

1
2
3
4
// 推论为string类型
let str = "chuckle"
// 不能再赋予其它类型
str = 123 // 不能将类型“number”分配给类型“string”。

若变量没有初始化,会在首次赋值时推断类型

1
2
3
let str
str = "qx"
str = 123 // 不能将类型“number”分配给类型“string”。

类型别名

type 定义类型别名,多用于复合类型

1
2
3
4
5
6
7
8
9
10
11
// 复合类型
type t1 = number | string
let s: t1
// 函数类型
type t2 = (name: string) => string
const fn1: t2 = (name) => {
return name
}
// 值别名,只能是value定义的值
type value = boolean | 0 | '123'
let a: value = 0

type 和 interface 区别:

  1. interface可以继承 type 只能通过 & 交叉类型合并
  2. type 可以定义联合类型、可以使用一些操作符 interface 不行
  3. interface 遇到重名的会合并 type 不行

type 中的 extends 表示包含,判断左值是否为右类型的子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type num1 = 1 extends number ? 1 : 0 // 1
type num2 = 1 extends string ? 1 : 0 // 0
type num3 = {} extends number ? 1 : 0 // 1
type num5 = 1 extends Object ? 1 : 0 // 1
type num6 = (()=>{}) extends object ? 1 : 0 // 1
type num7 = Number extends object ? 1 : 0 // 1
type num8 = number extends object ? 1 : 0 // 0
type num9 = Object extends object ? 1 : 0 // 1
type num10 = any extends object ? 1 : 0 // 1
type num11 = void extends Object | string | object ? 1 : 0 // 0
type num12 = void extends null | undefined ? 1 : 0 // 0
type num13 = null | undefined extends object ? 1 : 0 // 0
type num14 = null extends undefined ? 1 : 0 // 0
type num15 = undefined extends null ? 1 : 0 // 0
type num16 = undefined | null extends never ? 1 : 0 // 0
type num17 = never extends undefined & null ? 1 : 0 // 1

类型的层级:

1
2
3
4
5
6
object 包含所有非值类型(undefined,null除外)
any unknown
Object
Number String Boolean
number string boolean
never

never类型

never 类型来表示不应该存在的状态

1
2
3
4
5
6
7
8
9
10
type A = void | number | never // void | number
type B = number & string // never
// 因为必定抛出异常,所以 error 将不会有返回值
function error(message: string): never {
throw new Error(message);
}
// 因为存在死循环,所以 loop 将不会有返回值
function loop(): never {
while (true) {}
}

作用:便于TS检查一些可能存在的运行时的期望错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Value = 1 | 2 | 3 // 限制值
// type Value = 1 | 2 | 3 | 4 // 增加值而没有相对应的匹配
function choice(value: Value) {
switch (value) {
case 1:
break
case 2:
break
case 3:
break
// 进入default肯定有问题
default:
// 若增加了一个4,则编辑器报错:不能将类型“number”分配给类型“never”。
const error: never = value;
return error
}
}

泛型

泛型是动态类型,即在使用时确定类型,相比于暴力使用any,泛型即保留了方便的类型检查又增加了灵活性

1
2
3
4
5
function toArray<T>(a: T, b: T): Array<T> {
return [a, b]
}
toArray<number>(1, 2) // 完整写法,将类型传入<>
toArray(1, 2) // 自动推断

泛型有些类似类型的函数,先用个T作为形式类型占位,再由外部传入实际类型,或者让TS自动推断,所以<>中可以用多个形式类型占位

1
2
3
4
5
function toArray<T, K>(a: T, b: K): Array<T | K> {
return [a, b]
}
toArray<number, string>(1, "a") // 完整写法,将类型传入<>
toArray(1, "a") // 自动推断

除了在函数中,interface、type、class 等都可以使用泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// type
type AA<T> = number | string | T
let aa: AA<boolean> // 声明变量时确定第三个类型
aa = false
aa = 1
aa = "aa"
// interface
interface Data<T> {
msg: T
}
let d: Data<string> = { msg: "hello" }
// class
class Sub<T>{
attr: T[] = [];
add(a: T): T[] {
this.attr.push(a)
return this.attr
}
}
let ss = new Sub<number>()
ss.attr = [1, 2, 3]
ss.add(123)
let sss = new Sub<string>()
sss.attr = ['1', '2', '3']
sss.add('123')

一个常用的技巧:
因为 js 中的 number/string/boolean 这些基本量是传值的,而 object/array 这些对象传引用地址,当我们想在函数内部通过参数修改传进来的数据时,就得使用引用数据类型,所以我们把基本量包装成一个对象传进去,就可以实现在函数内部修改参数了

1
2
3
type ref<T> = {
value: T
}

泛型在封装请求方法时也经常使用:

data.json
1
2
3
4
5
6
7
8
{
"code": "0000",
"msg": "成功",
"data": {
"name": "qx",
"age": 20
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const axios = {
get<T>(url: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
}
}
xhr.send(null)
})
}
}

// 定义返回的数据类型
interface ResData {
code: string
msg: string
data: any
}

axios.get<ResData>('./data.json').then(res => {
console.log(res.data) // {name: 'qx', age: 20}
})

泛型约束keyof

在<>中使用extends约束泛型的类型范围

1
2
3
4
5
6
7
8
9
// function getLegnth<T>(arg:T) {
// return arg.length // 类型“T”上不存在属性“length”。
// }
interface Len {
length: number
}
function getLegnth<T extends Len>(arg: T) {
return arg.length
}

keyof 可以获取一个对象类型的所有键,作为联合类型

1
2
3
4
5
6
7
8
9
// function prop<T>(obj: T, key: string) {
// return obj[key] // 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "unknown"。
// }
function prop<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}
let o = { a: 1, b: 2, c: 3 }
prop(o, 'a')
// prop(o, 'd') // 类型“"d"”的参数不能赋给类型“"a" | "b" | "c"”的参数

更灵活的用法:

1
2
3
4
5
6
7
8
9
10
11
12
interface Info {
name: string
age: number
}
type InfoOptions<T extends object> = {
[Key in keyof T]?: T[Key]
}
type BB = InfoOptions<Info>
// type BB = {
// name?: string | undefined;
// age?: number | undefined;
// }

tsconfig.json

tsconfig.json 是TS的配置文件,通过 tsc --init 生成

配置详解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
"compilerOptions": {
"incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
"tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
"diagnostics": true, // 打印诊断信息
"target": "ES5", // 目标语言的版本
"module": "CommonJS", // 生成代码的模板标准
// "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
"lib": [
"DOM",
"ES5",
"ES6",
], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
"allowJs": true, // 允许编译器编译JS,JSX文件
"checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
"outDir": "./dist", // 指定输出目录
"rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
"declaration": true, // 生成声明文件,开启后会自动生成声明文件
"declarationDir": "./file", // 指定生成声明文件存放目录
// "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
"sourceMap": true, // 生成目标文件的sourceMap文件
// "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
"declarationMap": true, // 为声明文件生成sourceMap
"typeRoots": [], // 声明文件目录,默认时node_modules/@types
"types": [], // 加载的声明文件包
"removeComments": true, // 删除注释
"noEmit": true, // 不输出文件,即编译后不会生成任何js文件
"noEmitOnError": true, // 发送错误时不输出任何文件
"noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
"importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
"downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
"strict": true, // 开启所有严格的类型检查
"alwaysStrict": true, // 在代码中注入'use strict'
"noImplicitAny": true, // 不允许隐式的any类型
"strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
"strictFunctionTypes": true, // 不允许函数参数双向协变
"strictPropertyInitialization": true, // 类的实例属性必须初始化
"strictBindCallApply": true, // 严格的bind/call/apply检查
"noImplicitThis": true, // 不允许this有隐式的any类型
"noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
"noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
"noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
"noImplicitReturns": true, //每个分支都会有返回值
"esModuleInterop": true, // 允许export=导出,由import from 导入
"allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
"moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { // 路径映射,相对于baseUrl
// 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
// "jquery": [
// "node_modules/jquery/dist/jquery.min.js"
// ],
"@/*":[
"./"
]
},
"rootDirs": [
"src",
"out"
], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
"listEmittedFiles": true, // 打印输出文件
"listFiles": true // 打印编译的文件(包括引用的声明文件)
},
// 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件)
// "include": [
// "src/**/*"
// ],
// 指定一个排除列表(include的反向操作)
// "exclude": [
// "demo.ts"
// ],
// 指定哪些文件使用该配置(属于手动一个个指定文件)
// "files": [
// "demo.ts"
// ]
}

命名空间

namespace 定义命名空间,内部成员默认私有,通过 export 暴露

作用:组织代码,避免命名冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace A {
// export暴露命名空间内部成员
export const B = 123;
// 默认是命名空间私有成员
const C = 456;
// 可以嵌套使用
export namespace AA {
export const BB = B + C;
}
}
console.log(A.B); // 123
console.log(A.AA.BB); // 579
// 同名命名空间自动合并成员
namespace A {
export const D = 678;
}
console.log(A.D); // 678
// 简化命名空间
import X = A.AA;
console.log(X.BB); // 579

三斜线指令

三斜线指令是包含单个XML标签的单行注释。注释的内容会做为编译器指令使用。

TS3.0后建议用import

<reference path="..." /> 类似import,导入依赖,但无需export先暴露

1
2
3
///<reference path="./1.ts" />
///<reference path="./2.ts" />
console.log(X1.A, X2.A);

<reference types="node" /> 声明文件引入,表明这个文件使用了 @types/node/index.d.ts 里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。

声明文件d.ts

.d.ts (declare),TS的声明文件。对于第三方纯js库,通过d.ts可以获得完整的TS静态类型检查、提示、补全等功能

1
2
3
4
5
6
7
declare var // 声明全局变量
declare function // 声明全局方法
declare class // 声明全局类
declare enum // 声明全局枚举类型
declare namespace // 声明(含有子属性的)全局对象
interface 和 type // 声明全局类型
/// <reference /> 三斜线指令

TS会解析项目中所有的 *.ts 文件,当然也包含 .d.ts,注意 files、include 和 exclude 配置

TS2.0后,还会默认的查看 node_modules/@types 文件夹,用于为 TypeScript 提供有关用 JavaScript 编写的 API(第三方库) 的类型信息

热门的JS第三方库通常社区已经维护了其声明文件,安装:npm i @types/<包名> -D

部分库如axios,已经在package.json中指定了声明文件

1
2
3
{
"types": "index.d.ts",
}

对于社区没有维护声明文件的第三方库,就需要通过declare module语法自己写了,declare关键字

以express为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import express from 'express'
const app = express()
const router = express.Router()
app.use('/api', router)
router.get('/list', (req, res) => {
res.json({
code: 200
})
})
app.listen(9001,()=>{
console.log(9001)
})

// express.d.ts
declare module 'express' {
interface Router {
get(path: string, cb: (req: any, res: any) => void): void
}
interface App {
use(path: string, router: any): void
listen(port: number, cb?: () => void): void
}
interface Express {
(): App
Router(): Router
}
const express: Express
export default express
}

也可以为项目中常用的类型编写一个声明文件,声明语句中只能定义类型,不能在声明语句中定义具体的实现

1
2
3
4
const num: ref<number> = {
value: 0,
}
x = 1;
index.d.ts
1
2
3
4
declare type ref<T> = {
value: T
}
declare let x:number;

Mixins混入

对象的混入:Object.assign 合并多个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Name {
name: string
}
interface Age {
age: number
}
interface Sex {
sex: number
}

let people1: Name = { name: "chuckle" }
let people2: Age = { age: 20 }
let people3: Sex = { sex: 1 }

// 推断为交叉类型const people: Name & Age & Sex
const people = Object.assign(people1,people2,people3)

类方法的混入:

TS并没有Mixins关键字,实现类的混入需要用赋值断言、写混入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class O1 {
name!: string;
getName(): void {
console.log(this.name);
}
}

class O2 {
age!: number;
getAge(): void {
console.log(this.age);
}
}

class O3 implements O1, O2 {
// O1
name: string = 'chuckle';
getName!: () => void // 待混入的方法
// O2
age: number = 20;
getAge!: () => void // 待混入的方法
}

applyMixins(O3, [O1, O2]);
let smartObj = new O3();
smartObj.getName();
smartObj.getAge();

function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
console.log(baseCtor.prototype);
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
// 挂载到原型上
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}

相较于继承,这样混入使得只有类方法被“继承”

装饰器

装饰器就是一个函数,可以注入到类、方法、属性、参数,对象上,扩展其功能,增加了代码的可读性,清晰地表达了意图。

这是一个实验性的功能,虽然TS5.0后已经正式推出

tsconfig.json
1
2
// 允许普通装饰器
"experimentalDecorators": true,

类装饰器

类装饰器ClassDecorator应用于类构造函数,可以用来监视,修改或替换类定义

1
2
3
type ClassDecorator = <TFunction extends Function>(
target: TFunction // 类的构造函数(也就是类本身)作为其唯一的参数
) => TFunction | void;

通过装饰器给类添加属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
const Base: ClassDecorator = (target: Function) => {
console.log(target); // [class CLS]
target.prototype.fn = () => {
console.log("chuckle");
}
}

@Base
class CLS { }

const cls = new CLS() as any;
cls.fn() // chuckle

返回值

如果类装饰器返回一个值,它会使用提供的构造函数(类)来替换类的声明。并且必须保证能覆盖原来类的属性和方法,所以通常继承原来的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
const greeter = new Greeter("world");
console.log(greeter);
// Greeter {
// property: 'property',
// hello: 'override',
// newProperty: 'new property'
// }

注意,应该返回一个匿名类,否则实例前面的类型提示是返回的类名,而不是原来的类名,这容易造成误解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let a: any = null;
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
class A extends constructor {
newProperty = "new property";
hello = "override";
}
a = A;
return A
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
const greeter = new Greeter("world");
console.log(greeter);
// A {
// property: 'property',
// hello: 'override',
// newProperty: 'new property'
// }
// 实际上并没有发生继承,装饰器返回的类直接覆盖了原来的类的声明,现在的 Greeter 和 A 指的都是同一个类
console.log(a === Greeter); // true

装饰器工厂

装饰器工厂是一个高阶函数(函数柯里化),外层的函数接受值,里层的函数最终接受类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Base = (name: string) => {
const fn: ClassDecorator = (target: Function) => {
console.log(target); // [class CLS]
target.prototype.name = name;
target.prototype.fn = () => {
console.log(name);
}
}
return fn;
}

@Base("chuckle")
class CLS {
constructor() {

}
}
const cls = new CLS() as any;
cls.fn() // chuckle

方法装饰器

方法装饰器MethodDecorator应用到方法的属性描述符上,用来监视,修改或者替换方法定义

1
2
3
4
5
type MethodDecorator = <T>(
target: Object, // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey: string | symbol, // 成员的名字(函数名)
descriptor: TypedPropertyDescriptor<T> // 成员的属性描述符
) => TypedPropertyDescriptor<T> | void; // 如果返回一个值,会被用作方法的属性描述符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const met:MethodDecorator = (...args) => {
console.log(args);
// [
// {},
// 'getName',
// {
// value: [Function: getName], // 原方法
// writable: true, // 可写
// enumerable: false, // 不可枚举
// configurable: true // 可配置
// }
// ]
}
class A {
@met
getName ():string {
return 'chuckle'
}
}
const a = new A();

实现对原方法的拦截加工:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function MethodInterceptor(params?: string): MethodDecorator {
return (target: Object, name: string | symbol, decr: PropertyDescriptor) => {
let temp = decr.value; // 保存原方法
// 拦截加工原方法,不能是箭头函数, 否则 this 指向不是类的实例
decr.value = function (...args: any) {
console.log('前置拦截');
console.log(this); // CLS { name: 'chuckle' }
temp.call(this, ...args) // 调用原方法
console.log('后置拦截');
}
}
}
class CLS {
name = 'chuckle'

@MethodInterceptor("chuckle")
fn(...args: any) {
console.log(args); // [ 1, 2, 3 ]
}
}
let cls = new CLS()
cls.fn(1, 2, 3)
// 前置拦截
// CLS {}
// [ 1, 2, 3 ]
// 后置拦截

实现GET装饰器

请求通过装饰器工厂传入的url,将返回的数据传给原方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function GET(url: string): MethodDecorator {
return (target: Object, name: string | symbol, decr: PropertyDescriptor) => {
const fnc = decr.value; // 保存原方法
fetch(url).then(res => res.json())
.then(data => {
fnc({
code: 200,
msg: 'success',
data
}) // 调用原方法
}).catch(err => {
fnc({
code: 500,
msg: err,
data: null
})
})
}
}
class CLS {
@GET('https://api.github.com/')
fn(result: any) {
console.log(result.code); // 200
console.log(result.msg); // success
console.log(result.data); // { current_user_url: ..... }
}
}
const cls = new CLS();

参数装饰器

参数装饰器应用于类构造函数或方法声明,没有返回值。用来监视一个方法的参数是否被传入。

1
2
3
4
5
type ParameterDecorator = (
target: Object, // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey: string | symbol | undefined, // 成员的名字(函数名)
parameterIndex: number // 参数在函数参数列表中的索引
) => void;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const currency: ParameterDecorator = (
target: Object,
key: string | symbol | undefined,
index: number
) => {
console.log(target === CLS.prototype) // true
console.log(target, key, index)
// {} setName 0
}

class CLS {
name = 'chuckle'
setName(@currency name: string) {
this.name = name
}
}

属性装饰器

属性装饰器应用较少,用来监视类中是否声明了某个名字的属性。

1
2
3
4
type PropertyDecorator = (
target: Object, // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey: string | symbol // 成员的名字(属性名)
) => void;
1
2
3
4
5
6
7
8
const met: PropertyDecorator = (target: Object, key: string | symbol) => {
console.log(target, key); // {} name
}

class CLS {
@met
name = 'chuckle'
}

顺序

多个同类型装饰器可以同时应用到一个声明上

1
2
3
4
5
6
// 书写在同一行上:
@f @g x
// 书写在多行上:
@f
@g
x

求值方式与复合函数相似。复合的结果(f ∘ g)(x)等同于f(g(x))。

当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作函数,由下至上依次调用(应用)。

装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

方法参数装饰器执行的优先级最高,而方法和属性装饰器受到定义顺序的影响。类与构造器参数装饰器优先级最低。

元数据

对象、类等都是数据,它们描述了某种数据,而描述这些数据的数据就是元数据

reflect-metadata 库通过添加对元数据的支持,使得装饰器能够更方便地访问和修改类和方法的元数据,也许还会提示安装tslib

安装:npm i reflect-metadata tslib

引入,扩展了全局API Reflect
1
import 'reflect-metadata'

在装饰器中可以拿到类、方法、访问符、属性、参数的基本信息,如名称,描述符等。获取更多信息就需要元数据。

在编译过程中产生的元数据是非常重要的信息,在 nestjs 框架中 DI 和 IOC 的实现就依赖了他们。

参考:
深入浅出Typescript装饰器
TS 装饰器(2): 元数据

定义/获取元数据

Reflect.defineMetadata()定义元数据:给对象、类等数据附加额外的描述信息,但又不会影响到数据本身。
Reflect.getMetadata() 获取元数据。

1
2
3
4
5
6
7
8
// 为类或对象定义元数据
Reflect.defineMetadata(key, value, classObject)
// 为classPrototype[methodName]方法定义元数据
Reflect.defineMetadata(key, value, classPrototype, methodName)
// 为classPrototype[propKey]属性定义元数据
Reflect.defineMetadata(key, value, classPrototype, propKey)
// 获取元数据
Reflect.getMetadata(metadataKey, target, propertyKey?)

前两个参数分别是元数据的key和value,都是any类型,后面的参数设定了要将元数据定义给谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj = { 
name: 'chuckle',
fn(){
console.log(this.name);
}
}
// 定义在obj对象上
Reflect.defineMetadata('testKey1', 'testValue1', obj)
// 定义在obj对象的name属性上
Reflect.defineMetadata('testKey2', 'testValue2', obj, 'name')
// 定义在obj对象的fn方法上
Reflect.defineMetadata('testKey3', 'testValue3', obj, 'fn')

// 获取obj对象上的元数据
console.log(Reflect.getMetadata('testKey1', obj)) // testValue2
// 获取obj对象的name属性上的元数据
console.log(Reflect.getMetadata('testKey2', obj, 'name')) // testValue2
// 获取obj对象的fn方法上的元数据
console.log(Reflect.getMetadata('testKey3', obj, 'fn')) // testValue3

// 不存在的元数据,返回undefined
console.log(Reflect.getMetadata('test', obj)) // undefined

元数据可以定义在不存在的对象成员上,并且能正常获取

1
2
3
const obj = {}
Reflect.defineMetadata('testKey', 'testValue', obj, 'name')
console.log(Reflect.getMetadata('testKey', obj, 'name')) // testValue

在类中定义

Reflect.defineMetadata多给普通对象定义元数据

类使用 @Reflect.metadata 元数据装饰器,更方便地定义元数据,参数只有key和value

类元数据直接定义在类的构造函数上,而类成员的元数据定义在原型对象对应的属性上(无论实际上是否存在该属性),实例也会被赋予类成员元数据的副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Reflect.metadata('testKey1', 'testValue1')
class CLS {

@Reflect.metadata('testKey2', 'testValue2')
name = 'chuckle'

@Reflect.metadata('testKey3', 'testValue3')
fn() {
console.log(this.name);
}
}
const cls = new CLS();
console.log(Reflect.getMetadata('testKey1', CLS)) // testValue1
console.log(Reflect.getMetadata('testKey2', CLS.prototype, 'name')) // testValue2
console.log(Reflect.getMetadata('testKey3', CLS.prototype, 'fn')) // testValue3
// 实例也会被赋予类成员的元数据的副本
console.log(Reflect.getMetadata('testKey2', cls, 'name')) // testValue2

其它方法

Reflect还有一些方法

  1. hasMetadata 判断指定目标是否存在指定key的元数据
  2. hasOwnMetadata 判断指定目标是否存在指定key的元数据
  3. getMetadataKeys 获取指定目标的所有的元数据key组成的数组,包括其父类的元数据key
  4. getOwnMetadataKeys 获取指定目标自身的所有的元数据key组成的数组,不包括其父类的元数据key
1
2
3
4
5
6
7
8
9
10
@Reflect.metadata('key1', 'value1')
class Parent{ }
@Reflect.metadata('key2', 'value2')
class Child extends Parent{ }

console.log(Reflect.getMetadataKeys(Child)) // [ 'key2', 'key1' ]
console.log(Reflect.getOwnMetadataKeys(Child)) // [ 'key2' ]

console.log(Reflect.hasMetadata('key1', Child)) // true
console.log(Reflect.hasOwnMetadata('key1', Child)) // false

内置元数据

开启emitDecoratorMetadata配置,TS 会在编译后自动给类和类成员添加如下元数据:

  1. design:type:被装饰目标的类型
    • 成员属性:属性的标注类型
    • 成员方法:Function 类型
  2. design:paramtypes: 被装饰目标的参数类型
    • 成员方法:方法形参列表的标注类型
    • 类:构造函数形参列表的标注类型
  3. design:returntype
    • 成员方法:函数返回值的标注类型
tsconfig.json
1
2
// 允许自动添加内置元数据
"emitDecoratorMetadata": true,

注意:

  1. 只有你已经给目标添加了元数据,TS才会自动为这些目标添加内置元数据。
  2. 标注类型,即在代码中显式标注参数、返回值、属性等的类型,而不能靠自动推断。

通过 design:paramtypes 可以获取到方法中有多少个参数,每个参数的类型等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Reflect.metadata('key', 'value')
class People {
@Reflect.metadata('key', 'value')
name: string

constructor(name: string, age: number) {
this.name = name
}

@Reflect.metadata('key', 'value')
setName(name: string): string{
this.name = name
return this.name
}
}
// 获取标注的参数信息
console.log(Reflect.getMetadata('design:paramtypes', People))
// [ [Function: String], [Function: Number] ]
console.log(Reflect.getMetadata('design:paramtypes', People.prototype, 'setName'))
// [ [Function: String] ]

// 获取标注的返回值信息
console.log(Reflect.getMetadata('design:returntype', People.prototype, 'setName'))
// [Function: String]

// 获取标注的属性类型
console.log(Reflect.getMetadata('design:type', People.prototype, 'name'))
// [Function: String]

总之就是将你在代码中显式标注的类型信息,转为对应元数据定义在对应的类或类成员上

元数据的继承

元数据可以很好的适配于类之间的继承关系,父类会将其元数据的副本赋予子类,包括类本身和类成员的元数据

1
2
3
4
5
6
7
class Parent {
@Reflect.metadata('aa', 'aa')
aa = 1
}
class Child extends Parent { }
console.log(Reflect.getMetadataKeys(Child.prototype, 'aa'))
// [ 'design:type', 'aa' ]

搭建TS环境

使用Rollup、Webpack等打包构建工具开发TS项目

Rollup

详见:Rollup#配置TS环境

我的tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
{
"compilerOptions": {
"incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
"tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
"diagnostics": true, // 打印诊断信息
"target": "esnext", /* 指定 ECMAScript 目标版本:'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "esnext", /* 输出的代码使用什么方式进行模块化: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ /* 指定引用的标准库 */
"esnext",
"dom",
"dom.iterable",
], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
"allowJs": true, // 允许编译器编译JS,JSX文件
"checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
"outDir": "./dist", // 指定输出目录
"rootDir": "./src", // 指定输出文件目录(用于输出),用于控制输出目录结构
"declaration": true, // 生成声明文件,开启后会自动生成声明文件
"declarationDir": "./dist/typings", // 指定生成声明文件存放目录
// "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
"sourceMap": false, // 生成目标文件的sourceMap文件
// "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
"declarationMap": false, // 为声明文件生成sourceMap
// "typeRoots": [], // 声明文件目录,默认时node_modules/@types
"types": [], // 加载的声明文件包
"removeComments": true, // 删除注释
"noEmit": true, // 不输出文件,即编译后不会生成任何js文件
"noEmitOnError": true, // 发送错误时不输出任何文件
"noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
"importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
"downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
"strict": true, // 开启所有严格的类型检查
"alwaysStrict": true, // 在代码中注入'use strict'
"noImplicitAny": true, // 不允许隐式的any类型
"strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
"strictFunctionTypes": true, // 不允许函数参数双向协变
"strictPropertyInitialization": true, // 类的实例属性必须初始化
"strictBindCallApply": true, // 严格的bind/call/apply检查
"noImplicitThis": true, // 不允许this有隐式的any类型
"noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
"noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
"noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
"noImplicitReturns": true, //每个分支都会有返回值
"esModuleInterop": true, // 允许export=导出,由import from 导入
"allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
"moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { // 路径映射,相对于baseUrl
// 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
// "jquery": [
// "node_modules/jquery/dist/jquery.min.js"
// ],
"@/*": [
"src/*"
]
},
"rootDirs": [
"src"
], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
"listEmittedFiles": true, // 打印输出文件
"listFiles": true, // 打印编译的文件(包括引用的声明文件)
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true
},
// 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件)
"include": [
"src/**/*",
],
// 指定一个排除列表(include的反向操作)
// "exclude": [
// "demo.ts"
// ],
// 指定哪些文件使用该配置(属于手动一个个指定文件)
// "files": [
// "demo.ts"
// ]
}

Webpack

详见:Webpack#配置TS环境

esbuild+swc

安装:npm i @swc/core esbuild @swc/helpers -D

@swc/core 是 swc 的核心包,用于编译 JavaScript 和 TypeScript 代码;esbuild 是一个快速的 JavaScript 和 TypeScript 构建工具;@swc/helpers 是 swc 的辅助包,用于转换 JSX 代码

运行:node ./config.mjs

config.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import esbuild from 'esbuild'//打包工具
import swc from '@swc/core'//类似于babel es6 转 es5
import fs from 'node:fs'
esbuild.build({
entryPoints: ['./src/index.ts'], //入口文件
bundle: true, //模块单独打包
loader: {
'.js': 'js',
'.ts': 'ts',
'.jsx': 'jsx',
'.tsx': 'tsx',
},
treeShaking:true,
define: {
'process.env.NODE_ENV': '"production"',
},
plugins: [
{
//实现自定义loader
name: "swc-loader",
setup(build) {
build.onLoad({ filter: /\.(js|ts|tsx|jsx)$/ }, (args) => {
// console.log(args);
const content = fs.readFileSync(args.path, "utf-8")
const { code } = swc.transformSync(content, {
filename: args.path
})
return {
contents: code
}
})
},
}
],
outdir: "dist"
})

实战

写一些小玩意练练手

封装LocalStorage

实验 Rollup + TS 封装一个支持过期时间的LocalStorage

src/enum/index.ts
1
2
3
4
5
6
7
// 字典
export enum Dictionaries {
// 过期时间key
expire = '__expire__',
// 永不过期
permanent = 'permanent'
}
src/types/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Dictionaries } from "../enum";
export type Key = string // key类型
// 永不过期 | 过期时间的时间戳
export type expire = Dictionaries.permanent | number
// LocalStorage数据结构
export interface Data<T> {
value: T,
[Dictionaries.expire]: expire
}
// 获取LocalStorage结果
export interface Result<T> {
message: string,
value: T | null
}
// 定义封装LocalStorage类
export interface StorageCls {
set: <T>(key: Key, value: T, expire: expire) => void
get: <T>(key: Key) => Result<T | null>
remove: (key: Key) => void
clear: () => void
}
src/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { Dictionaries } from "./enum";
import { Key, Result, StorageCls, expire, Data } from "./types";

class LLS implements StorageCls {
set<T = any>(key: Key, value: T, expire: expire): void {
const data: Data<T> = {
value,
[Dictionaries.expire]: expire
}
localStorage.setItem(key, JSON.stringify(data))
}

get<T = any>(key: Key): Result<T | null> {
const data: Data<T> | null = JSON.parse(localStorage.getItem(key) || 'null')
const result: Result<T | null> = {
message: "",
value: null
}
if (data === null) {
result.message = `找不到${key}`
} else if (this.isOverdue(data[Dictionaries.expire])) {
result.message = `${key}已过期`
this.remove(key)
} else {
result.message = `获取${key}成功`
result.value = data.value
}
return result
}

remove(key: string): void {
localStorage.removeItem(key)
}

clear(): void {
localStorage.clear()
}

private isOverdue(expire: expire): boolean {
const now = new Date().getTime()
if (expire === Dictionaries.permanent) {
return false
} else if (typeof expire === 'number') {
return expire < now ? true : false
}
return false
}
}

// 测试代码
const sl = new LLS()
// 5s后过期
sl.set<number>('a', 123, new Date().getTime() + 5000)
setInterval(() => {
const a = sl.get<number>('a')
console.log(a)
}, 500)

发布订阅模式

实现个类似addEventListener、Vue evnetBus的发布订阅模式Demo

一个消息可以挂载多个订阅方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 订阅方法
interface EventFun {
(...args: any[]): any
}
interface EventCls {
// 订阅消息
on(name: string, callback: EventFun): void
// 发布消息
emit(name: string, ...args: any[]): void
// 取消订阅
off(name: string, fn: EventFun): void
// 只订阅一次
once(name: string, fn: EventFun): void
}
type CallbackArr = Array<EventFun>
// 保存所有消息
interface EventList {
// 消息名:订阅的方法集合
[key: string]: CallbackArr,
}
class SubPub implements EventCls {
list: EventList
constructor() {
this.list = {}
}
on(name: string, callback: EventFun) {
const callbackList: CallbackArr = this.list[name] || [];
callbackList.push(callback)
this.list[name] = callbackList
}
emit(name: string, ...args: any[]) {
const callbackList: CallbackArr = this.list[name]
if (callbackList) {
if (callbackList.length <= 0) {
console.warn("该消息没有订阅者")
return;
}
callbackList.forEach(callback => {
callback.apply(this, args)
})
} else {
console.warn("没有该消息")
}
}
off(name: string, fn: EventFun) {
const callbackList: CallbackArr = this.list[name]
if (callbackList) {
if (callbackList.length <= 0) {
console.warn("该消息没有订阅者")
return;
}
const index = callbackList.findIndex(fns => fns === fn)
index > -1 ? callbackList.splice(index, 1) : null
} else {
console.warn("没有该消息")
}
}
once(name: string, fn: EventFun) {
const decor: EventFun = (...args) => {
fn.apply(this, args)
this.off(name, decor)
}
this.on(name, decor)
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const subPub = new SubPub()
// 测试on和off
subPub.emit('abc', 678) // 没有该消息
const fn: EventFun = (...arg) => {
console.log(arg);
}
subPub.on('abc', fn)
subPub.emit('abc', 131, true) // [ 131, true ]
subPub.emit('abc', 678, false, 'qx') // [ 678, false, 'qx' ]
subPub.off('abc', fn)
subPub.emit('abc', 321, 'qx') // 该消息没有订阅者
console.log("=======================");
// 测试once
subPub.emit('a', 678) // 没有该消息
subPub.once('a', (...arg) => {
console.log(arg);
})
subPub.emit('a', 678, 'abc') // [ 678, 'abc' ]
subPub.emit('a', 123, 'qx') // 该消息没有订阅者
subPub.on('a', (...arg) => {
console.log(arg);
})
subPub.emit('a', 123, 'qx') // [ 123, 'qx' ]

Proxy、Reflect

Proxy(代理)和Reflect(反射)是ES6为了操作对象而提供的新API,它们为开发者提供了对对象行为进行拦截和自定义的能力

Proxy和Reflect都有13个名字和参数一模一样的方法,因为对象的操作无非就get、set等

ES6推荐用Proxy去代理对对象的操作,用Reflect实现对对象的实际操作

基本操作

Proxy基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* @interface ProxyHandler
* @template T - 泛型,代表被代理对象的类型
*/
interface ProxyHandler<T extends object> {
/**
* 1、用于拦截函数调用操作。
*
* @param {T} target - 被代理的原始可调用对象。
* @param {*} thisArg - 函数调用时的原始 this 上下文。
* @param {Array} argArray - 传递给函数调用的参数数组。
* @returns {*} 函数调用的结果。
*/
apply?(target: T, thisArg: any, argArray: any[]): any;

/**
* 2、用于拦截 `new` 操作符,即构造函数的调用。
*
* @param {T} target - 被代理的原始对象。
* @param {Array} argArray - 传递给构造函数的参数数组。
* @param {Function} newTarget - 最初调用的构造函数。
* @returns {object} 构造函数调用的结果。
*/
construct?(target: T, argArray: any[], newTarget: Function): object;

/**
* 3、用于拦截 `Object.defineProperty()` 操作。
*
* @param {T} target - 被代理的原始对象。
* @param {string | symbol} property - 要定义或修改的属性的名称或 `Symbol`。
* @param {PropertyDescriptor} attributes - 属性的描述符。
* @returns {boolean} 一个布尔值,指示属性是否成功定义或修改。
*/
defineProperty?(target: T, property: string | symbol, attributes: PropertyDescriptor): boolean;

/**
* 4、用于拦截 `delete` 操作。
*
* @param {T} target - 被代理的原始对象。
* @param {string | symbol} p - 要删除的属性的名称或 `Symbol`。
* @returns {boolean} 一个布尔值,指示属性是否成功删除。
*/
deleteProperty?(target: T, p: string | symbol): boolean;

/**
* 5、用于拦截属性值的获取操作。
*
* @param {T} target - 被代理的原始对象。
* @param {string | symbol} p - 要获取的属性的名称或 `Symbol`。
* @param {*} receiver - 代理或从代理继承的对象。
* @returns {*} 属性的值。
*/
get?(target: T, p: string | symbol, receiver: any): any;

/**
* 6、用于拦截 `Object.getOwnPropertyDescriptor()` 操作。
*
* @param {T} target - 被代理的原始对象。
* @param {string | symbol} p - 要获取属性描述符的属性的名称或 `Symbol`。
* @returns {PropertyDescriptor | undefined} 属性的描述符或 `undefined`。
*/
getOwnPropertyDescriptor?(target: T, p: string | symbol): PropertyDescriptor | undefined;

/**
* 7、用于拦截 `[[GetPrototypeOf]]` 内部方法。
*
* @param {T} target - 被代理的原始对象。
* @returns {object | null} 对象的原型或 `null`。
*/
getPrototypeOf?(target: T): object | null;

/**
* 8、用于拦截 `in` 操作。
*
* @param {T} target - 被代理的原始对象。
* @param {string | symbol} p - 要检查存在性的属性的名称或 `Symbol`。
* @returns {boolean} 一个布尔值,指示属性是否存在。
*/
has?(target: T, p: string | symbol): boolean;

/**
* 9、用于拦截 `Object.isExtensible()` 操作。
*
* @param {T} target - 被代理的原始对象。
* @returns {boolean} 一个布尔值,指示对象是否可扩展。
*/
isExtensible?(target: T): boolean;

/**
* 10、用于拦截 `Reflect.ownKeys()` 操作。
*
* @param {T} target - 被代理的原始对象。
* @returns {ArrayLike<string | symbol>} 一个可迭代对象,包含对象自身的所有属性键。
*/
ownKeys?(target: T): ArrayLike<string | symbol>;

/**
* 11、用于拦截 `Object.preventExtensions()` 操作。
*
* @param {T} target - 被代理的原始对象。
* @returns {boolean} 一个布尔值,指示对象是否成功变为不可扩展。
*/
preventExtensions?(target: T): boolean;

/**
* 12、用于拦截属性值的设置操作。
*
* @param {T} target - 被代理的原始对象。
* @param {string | symbol} p - 要设置的属性的名称或 `Symbol`。
* @param {*} newValue - 要设置的新值。
* @param {*} receiver - 最初分配操作的对象。
* @returns {boolean} 一个布尔值,指示属性是否成功设置。
*/
set?(target: T, p: string | symbol, newValue: any, receiver: any): boolean;

/**
* 13、用于拦截 `Object.setPrototypeOf()` 操作。
*
* @param {T} target - 被代理的原始对象。
* @param {object | null} v - 对象的新原型或 `null`。
* @returns {boolean} 一个布尔值,指示是否成功设置对象的原型。
*/
setPrototypeOf?(target: T, v: object | null): boolean;
}

Reflect基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
declare namespace Reflect {
/**
* 1、调用具有指定对象作为 this 值和指定数组的元素作为参数的函数。
*
* @param {Function} target - 要调用的函数。
* @param {*} thisArgument - 用作 this 对象的对象。
* @param {Array} argumentsList - 传递给函数的参数值的数组。
* @returns {*} 函数调用的结果。
*/
function apply<T, A extends readonly any[], R>(
target: (this: T, ...args: A) => R,
thisArgument: T,
argumentsList: Readonly<A>,
): R;
function apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any;

/**
* 2、使用指定数组的元素作为参数以及指定的构造函数作为 `new.target` 值构造目标。
*
* @param {Function} target - 要调用的构造函数。
* @param {Array} argumentsList - 传递给构造函数的参数值的数组。
* @param {Function} newTarget - 用作 `new.target` 对象的构造函数。
* @returns {object} 构造函数调用的结果。
*/
function construct<A extends readonly any[], R>(
target: new (...args: A) => R,
argumentsList: Readonly<A>,
newTarget?: new (...args: any) => any,
): R;
function construct(target: Function, argumentsList: ArrayLike<any>, newTarget?: Function): any;

/**
* 3、向对象添加属性或修改现有属性的属性。
*
* @param {object} target - 要添加或修改属性的对象。这可以是原生 JavaScript 对象
* (即用户定义的对象或内置对象)或 DOM 对象。
* @param {PropertyKey} propertyKey - 属性名称。
* @param {PropertyDescriptor & ThisType<any>} attributes - 属性的描述符。可以是数据属性或访问器属性。
* @returns {boolean} 一个布尔值,指示属性是否成功定义或修改。
*/
function defineProperty(target: object, propertyKey: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): boolean;

/**
* 4、从对象中删除属性,相当于 `delete target[propertyKey]`,除非 `target[propertyKey]` 是不可配置的,否则不会抛出异常。
*
* @param {object} target - 从中删除自有属性的对象。
* @param {PropertyKey} propertyKey - 属性名称。
* @returns {boolean} 一个布尔值,指示属性是否成功删除。
*/
function deleteProperty(target: object, propertyKey: PropertyKey): boolean;

/**
* 5、获取目标的属性,相当于 `target[propertyKey]` 当 `receiver === target` 时。
*
* @param {object} target - 包含属性的对象,可以是直接在对象上定义的属性,也可以是原型链中继承的属性。
* @param {PropertyKey} propertyKey - 属性名称。
* @param {unknown} receiver - 作为 getter 函数中的 `this` 值使用的引用,
* 如果 `target[propertyKey]` 是访问器属性。
* @returns {*} 属性的值。
*/
function get<T extends object, P extends PropertyKey>(
target: T,
propertyKey: P,
receiver?: unknown,
): P extends keyof T ? T[P] : any;

/**
* 6、获取指定对象的自有属性描述符。
* 自有属性描述符是直接在对象上定义的,而不是从对象的原型继承的描述符。
*
* @param {object} target - 包含属性的对象。
* @param {PropertyKey} propertyKey - 属性名称。
* @returns {TypedPropertyDescriptor<P extends keyof T ? T[P] : any> | undefined}
* 属性的描述符或 `undefined`。
*/
function getOwnPropertyDescriptor<T extends object, P extends PropertyKey>(
target: T,
propertyKey: P,
): TypedPropertyDescriptor<P extends keyof T ? T[P] : any> | undefined;

/**
* 7、返回对象的原型。
*
* @param {object} target - 引用原型的对象。
* @returns {object | null} 对象的原型或 `null`。
*/
function getPrototypeOf(target: object): object | null;

/**
* 8、等效于 `propertyKey in target`。
*
* @param {object} target - 包含属性的对象,可以是直接在对象上定义的属性,也可以是原型链中继承的属性。
* @param {PropertyKey} propertyKey - 属性名称。
* @returns {boolean} 一个布尔值,指示属性是否存在。
*/
function has(target: object, propertyKey: PropertyKey): boolean;

/**
* 9、返回一个值,指示是否可以向对象添加新属性。
*
* @param {object} target - 要测试的对象。
* @returns {boolean} 一个布尔值,指示对象是否可扩展。
*/
function isExtensible(target: object): boolean;

/**
* 10、返回对象的自有字符串和符号键。对象的自有属性是直接在该对象上定义的属性,
* 不是从对象的原型继承的属性。
*
* @param {object} target - 包含自有属性的对象。
* @returns {(string | symbol)[]} 包含对象自有的所有属性键的数组。
*/
function ownKeys(target: object): (string | symbol)[];

/**
* 11、防止向对象添加新属性。
*
* @param {object} target - 要使其不可扩展的对象。
* @returns {boolean} 一个布尔值,指示对象是否已成功变为不可扩展。
*/
function preventExtensions(target: object): boolean;

/**
* 12、设置目标的属性,相当于 `target[propertyKey] = value` 当 `receiver === target` 时。
*
* @param {object} target - 包含属性的对象,可以是直接在对象上定义的属性,也可以是原型链中继承的属性。
* @param {PropertyKey} propertyKey - 属性名称。
* @param {any} value - 要设置的新值。
* @param {any} receiver - 作为 setter 函数中的 `this` 值使用的对象,
* 如果 `target[propertyKey]` 是访问器属性。
* @returns {boolean} 一个布尔值,指示属性是否成功设置。
*/
function set<T extends object, P extends PropertyKey>(
target: T,
propertyKey: P,
value: P extends keyof T ? T[P] : any,
receiver?: any,
): boolean;
function set(target: object, propertyKey: PropertyKey, value: any, receiver?: any): boolean;

/**
* 13、设置指定对象的原型为对象 proto 或 null。
*
* @param {object} target - 要更改其原型的对象。
* @param {object | null} proto - 新原型的值,或 `null`。
* @returns {boolean} 一个布尔值,指示是否成功设置对象的原型。
*/
function setPrototypeOf(target: object, proto: object | null): boolean;
}

案例

判断成年的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let person = {
name: 'chuckle',
age: 20,
}
let personProxy = new Proxy(person, {
get(target, p, receiver) {
if (target.age < 18) {
return Reflect.get(target, p, receiver)
} else {
return '已成年'
}
}
})
console.log(personProxy.age) // 已成年
Reflect.set(person, 'age', 16)
console.log(personProxy.age) // 16

实现简单的响应式Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const ref = <T extends object>(params: T): T => {
return new Proxy(params, {
get(target, key, receiver): any {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver): boolean {
// 获取原来的值
const oldValue = Reflect.get(target, key, receiver);
// 输出变化,或做一些响应式操作
console.log(`set [${String(key)}] from "${oldValue}" to "${value}"`);
// 映射实际操作
const result = Reflect.set(target, key, value, receiver);
return result;
}
})
};
const person = ref({
name: 'chuckle',
age: 20,
});
console.log(person); // { name: 'chuckle', age: 20 }
person.age = 16; // set [age] from "20" to "16"

类型兼容性

协变(鸭子类型)

协变:鸭子类型在程序设计中是动态类型的一种风格。
在这种风格中,一个物件有效的语义,不是由继承自特定的类或实现特定的接口决定,而是由当前方法和属性的集合决定

一只鸟走路像鸭子,游泳也像,做什么都像,那么这只鸟就可以成为鸭子类型。

当子类型中的属性满足父类型就可以进行赋值,即协变不能少可以多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Bird {
name: string;
}
interface Duck {
name: string;
say(): void
}
let bird: Bird = {
name: '鸟',
}
let duck: Duck = {
name: '鸭子',
say() {
console.log(this.name);
}
}
bird = duck

Duck类型包含了Bird类型,且比Bird类型更具体,属性更多,从继承角度讲,Duck就是Bird的子类型,而子类型当然可以赋值给父类型,但会抛弃子类型中不属于父类型的多余属性

逆变

逆变同样是子类型赋给父类型,多发生在函数参数上

接收子类型的函数变量不能赋给接收父类型的函数变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Bird {
name: string;
}
interface Duck {
name: string;
say(): void
}
let fnBird = (params: Bird) => {
console.log(params.name);
}
let fnDuck = (params: Duck) => {
console.log(params.name);
params.say();
}
fnBird = fnDuck // 错误
// 不能将类型“(params: Duck) => void”分配给类型“(params: Bird) => void”。
// 参数“params”和“params” 的类型不兼容。
// 类型 "Bird" 中缺少属性 "say",但类型 "Duck" 中需要该属性。
fnDuck = fnBird // 允许

函数变量发生赋值后,调用fnBird(),实际上仍然调用的是(params: Duck)=>{},若是传入params: Bird,其成员不足以覆盖params: Duckparams.say()会报错,所以是不安全的,仍然要符合不能少可以多

双向协变

这是一种不安全的类型兼容操作,在TS2.0后需要关闭严格的函数类型检查,来允许函数参数双向协变

1
2
3
"compilerOptions": {
"strictFunctionTypes": false,
}

现在函数变量可以自由赋值

1
2
3
4
5
6
7
8
9
let fnBird = (params: Bird) => {
console.log(params.name);
}
let fnDuck = (params: Duck) => {
console.log(params.name);
params.say();
}
fnBird = fnDuck // 允许
fnDuck = fnBird // 允许

因为保存函数的仅仅是一个变量,赋值后到另一个函数变量后调用,再调用的仍然同一个函数,看起来没问题,但若是在赋值后再调用原本的变量调用函数,就会存在问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface Bird {
name: string;
}
interface Duck {
name: string;
say(): void
}
let bird: Bird = {
name: '鸟',
}
let duck: Duck = {
name: '鸭子',
say() {
console.log(this.name);
}
}
let fnBird = (params: Bird) => {
console.log(params.name);
}
let fnDuck = (params: Duck) => {
console.log(params.name);
params.say();
}
fnBird = fnDuck
fnBird(bird) // 运行时报错:params.say is not a function
fnDuck(duck)

fnBird在经过赋值后,实际的函数是fnDuck,需要的是Duck类型参数,但仍然传入的是Bird类型参数,父类型赋给子类型当然会出错

所以,不要使用双向协变,这可能导致运行时的意外错误

Set,Map

set和map是ES6新增的引用数据类型,详见:ES6查缺补漏

Set类似于数组,但自动去重

1
2
3
4
5
6
7
const arr: number[] = [1, 2, 3, 3, 2, 1];
const set: Set<number> = new Set(arr);
set.add(4);
console.log(set); // Set(4) { 1, 2, 3, 4 } 自动去重
set.delete(4);
console.log(set); // Set(3) { 1, 2, 3 }
console.log(set.has(4)); // false

Map类似对象,但键可以是任意类型

1
2
3
4
5
6
7
8
9
10
const arr: [any, any][] = [
['x', 1],
['y', 2],
]
const map: Map<any, any> = new Map(arr);
console.log(map); // Map(2) { 'x' => 1, 'y' => 2 }
map.set('z', 3);
console.log(map.get('z')); // 3
console.log(map.keys()); // [Map Iterator] { 'x', 'y', 'z' }
console.log(map.values()); // [Map Iterator] { 1, 2, 3 }

weakSet,weakMap

Weak在英语的意思是,表示weakSet的值、weakMap的键必须是引用类型,且引用是弱引用,不计入垃圾回收策略,如果没有其他的对Weak中对象的引用存在,那么这些对象会被垃圾回收

垃圾回收:JavaScript引擎在值“可达”和可能被使用时会将其保持在内存中,否则会自动进行垃圾回收

1
2
3
4
5
let john = { name: "John" };
// 该对象能被访问,john 是它的引用
// 覆盖引用
john = null;
// 该对象将会被从内存中清除

通常,当对象、数组之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都被认为是可达的。

例如,如果把一个对象放入到Map中作为键,那么只要这个Map存在,那么这个对象也就存在,即使没有其他对该对象的引用。

1
2
3
4
5
6
let map = new Map();
map.set(john, "...");
john = null; // 覆盖引用
// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它
console.log(map.keys()) // [Map Iterator] { { name: 'John' } }

WeakMap在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收

1
2
3
4
5
6
7
8
9
10
11
let john: any = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 覆盖引用
// john 被从内存中删除了!没有任何办法获取原来的对象
// weakMap没有keys()方法
// WeakMap 只有以下的方法:
// 1. weakMap.get(key)
// 2. weakMap.set(key, value)
// 3. weakMap.delete(key)
// 4. weakMap.has(key)

WeakMap和WeakSet在第三方库处理外部数据、或用作缓存时非常有用,可以防止垃圾数据堆积在内存中

1
2
3
4
5
6
7
8
9
10
11
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// 递增用户来访次数
function countUser(user: object) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}

let john: any = { name: "John" };
countUser(john); // count his visits
// john 离开了,WeakMap自动释放该键值对
john = null;

内置高级类型

TS内置了许多高级类型(常用的工具类型),可以看作是类型的函数,接收一个类型,返回加工后的类型

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 将 T 中的所有属性变为可选的
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

/**
* 将 T 中的所有属性变为必需的
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};

/**
* 将 T 中的所有属性变为只读的
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

/**
* 从 T 中挑选出键在联合类型 K 中的一组属性
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

/**
* 构造一个具有一组属性 K,属性类型为 T 的类型
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};

/**
* 从 T 中排除那些可分配给 U 的类型
*/
type Exclude<T, U> = T extends U ? never : T;

/**
* 去掉某些属性
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Partial

Partial<T>可以将原来的类型变为可选

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string
age: number,
gender: 'male' | 'female'
}
type p1 = Partial<Person>
// type p1 = {
// name?: string | undefined;
// age?: number | undefined;
// gender?: "male" | "female" | undefined;
// }

keyof获取一个对象类型的所有键,作为联合类型,in是映射类型语法,用于遍历keyof T让联合类型的每一项作为属性,? 将每一个属性变成可选项,T[P]索引访问操作符,与访问属性值的操作类似

1
2
3
type Partial<T> = {
[P in keyof T]?: T[P];
};

Pick

筛选类型的属性。从类型的属性中,选取指定一组属性,返回一个新的类型定义。

1
2
3
4
5
6
7
8
9
10
interface Person {
name: string
age: number,
gender: 'male' | 'female'
}
type p2 = Pick<Person, 'name' | 'age'>
// type p2 = {
// name: string;
// age: number;
// }

K extends keyof T限制key必须是传入类型的属性,in遍历K让联合类型的每一项作为属性,T[P]索引访问操作符

1
2
3
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

Readonly

将类型中所有属性变为只读

1
2
3
4
5
6
7
8
9
10
11
interface Person {
name: string
age: number,
gender: 'male' | 'female'
}
type p3 = Readonly<Person>
// type p3 = {
// readonly name: string;
// readonly age: number;
// readonly gender: 'male' | 'female';
// }

源码上和Partial很像,只是把 ? 换为了readonly

1
2
3
4
5
6
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// type Partial<T> = {
// [P in keyof T]?: T[P];
// };

Record

构造一个具有一组属性 K,属性类型为 T 的类型,做到同时对 key 和 value 进行类型定义

1
2
3
4
5
6
7
8
9
interface Person {
name: string
age: number,
gender: 'male' | 'female'
}
type persons = Record<string, Person>
// type persons = {
// [x: string]: Person;
// }

keyof any: any 可以代表任何类型。那么任何类型的 key 都可能为 string 、 number 或者 symbol 。所以自然 keyof any 为 string | number | symbol 的联合类型。
K extends keyof any: 类型约束,表示类型参数 K 必须是类型 string | number | symbol 的子集或相同类型。
[P in K] 表示对类型 K 中的每个属性键 P 进行映射(遍历)。其中 K 是一个联合类型或是一个具有多个属性的类型

1
2
3
4
5
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type keys = keyof any
// type keys = string | number | symbol

Omit

去掉某些属性

1
2
3
4
5
6
7
8
9
10
interface Person {
name: string
age: number,
gender: 'male' | 'female'
}
type p4 = Omit<Person, 'gender'>
// type p4 = {
// name: string;
// age: number;
// }
1
2
3
4
// 从 T 中排除那些可分配给 U 的类型 
type Exclude<T, U> = T extends U ? never : T;
// 去掉某些属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

ReturnType

ReturnType<T> 获取函数返回值的类型

1
2
3
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
const fn = () => 'hello'
type T = ReturnType<typeof fn>; // string

类型工具

内置高级类型就是类型工具,但还有一些常用的类型工具并没有内置,需要自己实现

1、将部分属性变为可选

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
name: string
age: number,
gender: 'male' | 'female'
}
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
type newPerson = Optional<Person, 'gender'>
// type newPerson = {
// name: string
// age: number,
// gender?: 'male' | 'female'
// }

占位符infer

infer关键字定义一个占位符,可以自动推断类型,简化代码

案例:传入一个类型,若是数组类型,则返回数组元素的类型组成的联合类型,否则原样返回

1
2
3
4
5
6
type TypeDeconstruct<T> = T extends Array<any> ? T[number] : T
// 在 TypeScript 中,对于任何具有数字索引签名的对象(比如数组),使用 [number] 可以获取其元素类型。
// 这是 TypeScript 的索引访问类型的一部分,用于表示通过索引访问数组、元组或对象属性的类型。
const arr = [1, 'a', 2, 'b'];
type A = TypeDeconstruct<typeof arr> // type A = string | number
type B = TypeDeconstruct<boolean> // type B = boolean

使用infer自动推断数组中元素的类型

1
2
3
4
5
type TypeDeconstruct<T> = T extends Array<infer U> ? U : T
// 使用infer定义的占位符U,自动推断数组中元素的类型作为联合类型
const arr = [1, 'a', 2, 'b'];
type A = TypeDeconstruct<typeof arr> // type A = string | number
type B = TypeDeconstruct<boolean> // type B = boolean

提取/删除元素

1、提取头部元素

1
2
3
type Arr = ['a','b','c']
type First<T extends any[]> = T extends [infer First,...any[]] ? First : []
type a = First<Arr> // type a = "a"

2、提取尾部元素

1
2
3
type Arr = ['a', 'b', 'c']
type Last<T extends any[]> = T extends [...any[], infer Last,] ? Last : []
type a = Last<Arr> // type a = "c"

3、剔除第一个元素 Shift

1
2
3
type Arr = ['a','b','c']
type Shift<T extends any[]> = T extends [unknown,...infer Rest] ? Rest : []
type a = Shift<Arr> // type a = ["b", "c"]

4、剔除尾部元素 Pop

1
2
3
type Arr = ['a','b','c']
type Pop<T extends any[]> = T extends [...infer Rest,unknown] ? Rest : []
type a = Pop<Arr> // type a = ["a", "b"]

递归

通过递归将类型数组逆序:

1
2
3
type Arr = ['a','b','c']
type ReveArr<T extends any[]> = T extends [infer First, ...infer rest] ? [...ReveArr<rest>, First] : T
type Res = ReveArr<Arr> // type Res = ["c", "b", "a"]

提取指定位置上的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Arr = ['a', 'b', 'c'];
// 简单写法,不用递归
// type Index<T extends any[], K extends number> = T[K];
// type aa = Index<Arr, 1>; // type aa = "b"

// 递归写法
// 获取类型数组指定位置上的元素,索引从1开始
type IndexOf<T extends any[], K extends number, Acc extends any[] = []> = {
// 如果 K 等于 0,说明已经达到了目标索引位置,返回 Acc 中的第一个元素
0: Acc[0];
// 否则,继续递归,Acc 中添加数组的第一个元素,同时 K 减 1
1: K extends 0 ? Acc[0] : IndexOf<Tail<T>, K, [Head<T>, ...Acc]>;
}[K extends Acc['length'] ? 0 : 1];
// 辅助类型:获取数组的第一个元素
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
// 辅助类型:获取数组的剩余部分
type Tail<T extends any[]> = T extends [infer _H, ...infer R] ? R : [];

type a = IndexOf<Arr, 1>; // type a = "a"
type b = IndexOf<Arr, 2>; // type b = "b"
type c = IndexOf<Arr, 3>; // type b = "c"