# Part1-TypeScript

## 一、TypeScript简介

TypeScript（下称TS） 是 JavaScript 的超集，TS其实就是类型化的 JavaScript（下称JS），它不仅支持 JS的所有特性，还在 JS的基础上添加了静态类型注解扩展。

## 二、TypeScript环境搭建

首先，确定Node版本，这里强调一下，本课程所有的项目均基于Node v14.17.0进行开发：

```shell
# 检查出版本没有14.17.0
$ nvm list

  * 12.10.0 (Currently using 64-bit executable)

# 安装14.17.0并检查
$ nvm install 14.17.0
$ nvm use 14.17.0
$ nvm list

  * 14.17.0 (Currently using 64-bit executable)
    12.10.0
```

检查得到Node版本为14.17.0即可。

全局安装TS

```shell
$ npm i typescript -g
# 或者使用yarn
$ yarn add global typescript
```

安装完成后检查TS版本：

```shell
$ tsc --version
```

如果可以出来一个版本号，就代表已经全局安装成功。

### 1、创建项目与文件转换

创建好一个空白项目（如：ts-study）后，用vscode打开，创建一个名为 `demo1.ts` 的文件：

```typescript
function fn(){
    let str: string = "你好世界";
    console.log(str);
}

fn();
```

然后打开终端，使用 `node demo1.ts` 运行该ts文件，会发现报错：

[![image-20211205142555467](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxxeq8gj30ji06o0t7.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxxeq8gj30ji06o0t7.jpg)

原因是ts需要转为js文件才能被识别。

因此，先使用 `tsc demo1.ts` 将文件进行转换，转换成功会看到当前目录下多了一个demo1.js，此时使用node去运行这份js文件，才能看到结果：

[![image-20211205142816104](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxqh8l1j30xh0migmx.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxqh8l1j30xh0migmx.jpg)

### 2、自动转换

如果每次都要先将ts文件转一次js文件，再运行js文件，那么过程太过繁琐，可以直接使用插件帮我们解决：

```shell
# 全局安装ts-node
$ npm i -g ts-node@8.5.4
```

安装成功后，删掉项目中的 `demo1.js`，然后直接在终端运行：

```shell
$ ts-node demo1.ts
```

如果可以直接得到运行结果，如下图，即代表运行成功。

[![image-20211205143255479](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxuvnvyj30iq01xjr8.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxuvnvyj30iq01xjr8.jpg)

## 三、简单数据类型

### 1、简单数据类型（掌握）

同JS一样，TS也有简单数据类型，而且大同小异。拿上述使用的代码：

```typescript
function fn(){
    let str: string = "你好世界";
    console.log(str);
}

fn();
```

这里 `str:string` 中，str是变量名称，`:string` 是指定这个字段的类型为字符串，如果你给它赋值任何其他数据类型，都是不被允许的：

[![image-20211205144103199](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxz1jbwj30jy07b74o.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxz1jbwj30jy07b74o.jpg)

由此，我们了解了TS相较于JS，更加注重变量的数据类型校验。剩余的TS简单数据类型还包括：

* 字符串string
* 数字number / bigint(一般较大的整数才会使用)
* 布尔值boolean
* 唯一值symbol

> 请注意：虽然number和bigint都表示数字，但是这两个类型不兼容。

### 2、静态类型检测（了解）

什么是静态类型呢？所谓静态类型，就是一个变量如果定义了它的类型，那么这个类型就不再允许修改。在编译时期，静态类型的编程语言即可准确地发现类型错误，这就是静态类型检测的优势。

来观看下面这段代码：

```typescript
let str: string = "你好世界";
str = 123;	// 错误的修改
```

在编译（转译）时期，TypeScript 编译器将通过对比检测变量接收值的类型与我们显示注解的类型，从而检测类型是否存在错误。如果两个类型完全一致，显示检测通过；如果两个类型不一致，它就会抛出一个编译期错误，告知我们编码错误，效果如下图所示。

[![image-20211205145301302](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxwlnmxj30im072t92.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxwlnmxj30im072t92.jpg)

这个静态类型检测在VsCode中已经完美集成，开发者只需要看提示即可了解需要修改的地方。

### 3、类型注释与推断（掌握）

当写完以下代码

```typescript
function fn(one, two){
    console.log(one + two);
}

let result = fn(1, 2);
```

然后鼠标移上形参one时，会得到一个提示。

[![image-20211205152650871](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxsp18sj30s705sgm1.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxsp18sj30s705sgm1.jpg)

当提示为any时，代表你需要给它写上类型：

```typescript
function fn(one: number, two: number){
    return one + two;
}

let result = fn(1, 2);
```

你加上的这两个number，就是**类型注释**。而此时，当你鼠标移上result时，就会得到一个新的提示：

[![image-20211205152510159](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxriakxj30ex05oaa8.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxriakxj30ex05oaa8.jpg)

这个提示告诉你result是个number，但我们并没有声明result是number呀！这就是**类型推断**。

## 四、复杂数据类型

同JS一样，TS也有复杂数据类型，包含以下几种：

* 数组类型Array
* 元组类型Tuple
* 特殊类型any、unknown、void、undefined、null、never、object

接下来将围绕这几种数据类型进行讲解。

### 1、数组类型Array（掌握）

一个数组的每一项其实是可以定义它的字段类型的：

```typescript
let arr1: number[] = [1,2,3]
let arr2: string[] = ["1", "2", "3"]
```

如果没有按规定赋值，就会报错：

[![image-20211205150101292](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxvs4eyj30na0350sz.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxvs4eyj30na0350sz.jpg)

以上代码也可以用这种方式表示：

```typescript
let arr1: Array<Number> = [1,2,3]
let arr2: Array<String> = ["1", "2", "3"]
```

> 注意：
>
> 建议优先使用第一种方式书写，否则在React的JSX语法中可能会冲突。下文中所有可以使用这类写法的，都不会讲。

当然，所有数组的操作也必须符合以上的数据类型定义：

```typescript
arr2.push("4");     // 正确
arr2.push(4);       // 错误
```

### 2、类型推断（掌握）

如果每个数组项都是不同的值，TS会自动给我们提示：

[![image-20211205151558357](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxtev2dj30gb034gln.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxtev2dj30gb034gln.jpg)

这就是类型推断。TS帮我们推断出这个数组共有多少种数据类型，因此你可以修改代码为：

```typescript
let arr: (string | number | boolean)[] = ["张三", true, 16];
```

如此，开发者便可以较为智能的书写代码。

### 3、元组类型Tuple（了解）

如果数组中的每一项也是个数组，应该怎么写呢？这时候就需要依赖元组Tuple。

元组最重要的特性是可以限制数组元素的个数和类型，它特别适合用来实现多值返回。在 JavaScript 中并没有元组的概念，作为一门动态类型语言，它的优势是天然支持多类型元素数组。

```typescript
let arr: [string, number][] = [
    ["张三", 13],
    ["李四", 14],
    ["王五", 15]
];
```

如果每个数组项不是数组，而是对象，那么写法如下：

```typescript
let arr: {name: string, age: number}[] = [
    {name: "刘备", age: 51},
    {name: "关羽", age: 51},
    {name: "张飞", age: 51}
]
```

### 4、任意类型any（掌握）

any 指的是一个任意类型，它是官方提供的绕过静态类型检测的方法。如：

```typescript
let arr: any[] = [11, "你好世界"]
```

> 注意：
>
> 非必要不使用any，这几乎违背了TS的出发点。

### 5、不确定变量unknown（了解）

unknown 是 TypeScript 3.0 中添加的一个类型，它主要用来描述类型并不确定的变量。在 3.0 以前的版本中，只有使用 any 才能满足这种动态类型场景。与 any 不同的是，unknown 在类型上更安全。比如我们可以将任意类型的值赋值给 unknown，但 unknown 类型的值只能赋值给 unknown 或 any。

```typescript
let str: unknown;
let num: string = str;  // 不能将类型“unknown”分配给类型“string”。
let abc: any = str;     // 没有报错
```

### 6、无返回值void（掌握）

当一个函数没有返回值时，需要加上void：

```typescript
function fn(one: number, two: number): void{
    console.log(123)
}

fn(1,2);
```

### 7、undefined与null（了解）

这两个没什么好讲的，此处省略。

### 8、never（了解）

never 表示永远不会发生值的类型。一个永远不会有返回值，或者产生死循环的函数，就可以加上never：

```typescript
// 只有产生报错才会产生错误返回
function fn1(str: string): never {
    throw Error(str);
}

// 只有条件为真才会产生值返回
function fn2(): never {
    while(true) {}
}
```

### 9、object（了解）

object 类型表示非原始类型的类型，即非 number、string、boolean、bigint、symbol、null、undefined 的类型。

### 10、类型断言（了解）

如果一段js代码，你已经可以很明确的知道返回值一定是某种数据类型，就可以使用as给它断言：

```typescript
const arr: number[] = [1,2,3,4]
const result: number = arr.find(num=>num>2) as number;
```

## 五、字面量类型（了解）

在 TypeScript 中，字面量不仅可以表示值，还可以表示类型，即所谓的字面量类型。

使用值所代表的数据类型来作为变量的数据类型，简单来说，即：**值也可以作为类型使用**。但要注意，目前只能是string、number和boolean三种数据类型可以写成字面量类型。

```typescript
const str: "你好世界" = "你好世界";
const num: 123 = 123;
const bool: true = true;
```

## 六、返回值类型（掌握）

在上文讲述void的时候，其实已经使用过了，大致如下：

```typescript
function fn(one: number, two: number): number{
    return one+two;
}

fn(1,2);
```

## 七、剩余参数（了解）

在 ES6 中，JavaScript 支持函数参数的剩余参数，它可以把多个参数收集到一个变量中。同样，在TypeScript 中也支持这样的参数类型定义。

```typescript
// 参数个数不确定，但要实现累加
function fn(num1: number, ...nums: number[]): number{
    return nums.reduce((prev,next)=>prev+next, num1);
}

console.log( fn(1,2) );
console.log( fn(1,2,3) );
```

效果如图：

[![image-20211205160442935](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxuav5rj30jq02ndfo.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxuav5rj30jq02ndfo.jpg)

## 八、this（了解）

原生JS中的this指向非常模糊，函数中的this只有当函数被调用了才能知道this指向谁。而TS中，严格模式下必须显式地指定this指向，否则如下图所示：

[![image-20211205161843410](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfy0t1juj30s60c0wf4.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfy0t1juj30s60c0wf4.jpg)

因此代码应该改为：

```typescript
function fn(this: Window, str: string){		// 这里Window是大写
    console.log(this);
}
window.fn = fn;			// 这里window是小写
window.fn("你好世界");
```

这里要注意：

1、这段代码不会成功执行，因为你是在ts-node环境下运行，不存在window，也不会有global；

2、这里的this是伪形参，不会被编译为js。

## 九、ES6面向对象

ES5的面向对象是由函数实现，加上TS的话按照上述内容即可。而ES6的面向对象是使用class来定义类，因此需要特殊讲一下：

### 1、类（掌握）

以下描述一个Animal类：

```typescript
class Animal {
    name: string;   // 声明了Animal类中的name属性必须为string类型
    constructor(name: string){
        this.name = name;
    }
    sayName():void{	// 加void表示没有返回值
        console.log(this.name);
    }
}

let animal = new Animal("嘟嘟");
animal.sayName();   // 嘟嘟
```

### 2、继承（掌握）

定义一个Cat类，继承自Animal类：

```typescript
class Animal {
    name: string;   // 声明了Animal类中的name属性必须为string类型
    constructor(name: string){
        this.name = name;
    }
    sayName():void{
        console.log(this.name);
    }
}

class Cat extends Animal {
    constructor(name: string){
        super(name);
    }
    shout(): void{
        console.log(`${this.name}正在喊“喵喵”`);
    }
}

let cat = new Cat("嘟嘟");
cat.shout();   // 嘟嘟正在喊“喵喵”
```

### 3、公共、私有与受保护的修饰符（了解）

类属性和方法除了可以通过 extends 被继承之外，还可以通过修饰符控制可访问性。

在 TypeScript 中就支持 3 种访问修饰符，分别是 public、private、protected。

#### a. 公共修饰符public

public 修饰的是在任何地方可见、公有的属性或方法。上述代码中，凡是没有加修饰符的，都是默认已经自带public：

```typescript
class Animal {
    name: string;
    constructor(name: string){
        this.name = name;
    }
    sayName():void{
        console.log(this.name);
    }
}

let animal = new Animal("张三");
animal.sayName();

// 以上代码相当于：

class Animal {
    public name: string;
    constructor(name: string){
        this.name = name;
    }
    public sayName():void{
        console.log(this.name);
    }
}

let animal = new Animal("张三");
animal.sayName();
```

#### b. 私有修饰符private

private 修饰的是仅在当前类中可见、私有的属性或方法。具体如下：

```typescript
class Animal {
    public name: string;
    private age: number;	// 只能在当前类中使用，实例和子类都不能用
    constructor(name: string, age: number){
        this.name = name;
        this.age = age;
    }
    public sayName():void{
        console.log(`${this.name}今年${this.age}岁`);
    }
}

let animal = new Animal("张三", 11);
animal.sayName();           // 张三今年11岁
console.log(animal.age);    // 属性“age”为私有属性，只能在类“Animal”中访问 ✖
```

#### c. 受保护修饰符protected

protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法。

```typescript
class Animal {
    public name: string;
    protected age: number;  // 只能在当前类和子类中使用，实例不能用
    constructor(name: string, age: number){
        this.name = name;
        this.age = age;
    }
}

let animal = new Animal("张三", 11);
console.log(animal.age);    // 属性“age”受保护，只能在类“Animal”及其子类中访问 ✖

class Cat extends Animal {
    constructor(name: string, age: number){
        super(name, age);
    }
    shout(): void{
        console.log(`${this.name}正在喊“喵喵”，它今年${this.age}岁`);	 // 可以使用 ✔
    }
}

let cat = new Cat("嘟嘟", 11);
cat.shout();   // 嘟嘟
```

### 4、只读属性与静态属性（了解）

#### a. 只读属性readonly

加上readonly的属性只能读不能写：

[![image-20211205165223243](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfy1v5e4j30qv08rjs5.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfy1v5e4j30qv08rjs5.jpg)

#### b. 静态属性static

加上static的属性与方法，无需实例化即可调用：

```typescript
class Animal {
    // 直接给静态属性赋值
    static username: string = "张三";
}

Animal.username; // 张三
```

### 5、抽象类abstract（了解）

简单一句话：抽象类无法被实例化。

```typescript
// 定义一个抽象类
abstract class Animal {
    name: string;
    constructor(name:string){
        this.name = name;
    }
}

let animal = new Animal("张三");    // 无法创建抽象类的实例。
```

## 十、接口类型

请先看这段js代码：

```js
function fn({name, age}){
    console.log(`${name}的年龄为：${age}`)
}

fn({name: "张三", age: 30});	// 张三的年龄为：30
```

它的作用是给函数传了个对象，并且把对象中两个字段解构出来使用。但如果你要指定两个字段的类型，就会报错了：

[![image-20211205171331006](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxygzlbj30le06aaaf.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfxygzlbj30le06aaaf.jpg)

此时如果你通过以下代码书写，效果就不一样了：

```typescript
function fn({name, age}: {name: string; age: number}){
    console.log(`${name}的年龄为：${age}`)
}

fn({name: "张三", age: 30});	// 张三的年龄为：30
```

### 1、接口类型

其实这段代码是接口类型的演变，原来的表现形式应该如此：

```typescript
// 使用接口定义字段类型
// 接口一般首字母大写，经常使用I开头
interface IObj {
    name: string;
    age: number;
}

function fn(obj: IObj): void{
    console.log(`${obj.name}今年${obj.age}岁`)
}

fn({name: "张三", age: 33});	// 张三今年33岁
```

以上接口中，name和age的顺序是可以打乱的，不会影响代码执行。

### 2、可缺省属性

接口中定义的字段，参数必须传，但有些参数我们希望可传可不传，可以如下：

```typescript
interface IObj {
    name: string;
    age?: number;	// ?表示该参数可传可不传
}

function fn(obj: IObj): void{
    console.log(`${obj.name}今年${obj.age || 0}岁`)
}

fn({name: "张三"})	// 张三今年0岁
```

### 3、任意属性

其实属性名称有时也不确定，因此可以使用以下方式定义，但值必须为any。

```typescript
interface IObj {
    name: string;
    // [propName: string]: string | number;
    // 这个参数名称可能也是不确定的，也不清楚它的类型，可以any，也可指定若干数据类型，建议直接any
    [propName: string]: any; 
}

function fn(obj: IObj): void{
    console.log(`${obj.name}是${obj.sex || '男'}性`)
    
}

fn({name: "张三"})				// 张三是男性
fn({name: "张三", sex: "女"})	   // 张三是女性
```

> 注意：
>
> **任意属性和可选属性最好不能共存**，即便共存最好类型一致。

## 十一、泛型

泛型是指在定义函数、接口，或者类的时候，不预先指定具体的类型，而是在使用的时候在指定类型的一种特性。

现在有一个需求：有一个函数可以创建指定长度的数组，每一项都要填充一个指定值。

```typescript
function createArr(length: number, value: any): Array<any>{
// 或：function createArr(length: number, value: any): any[]{
    let arr = [];
    for(var i=0;i<length;i++){
        arr[i] = value;
    }
    return arr;
}

console.log( createArr(3, "猴子的救兵") );  // [ '猴子的救兵', '猴子的救兵', '猴子的救兵' ]
```

以上代码在函数调用前，是不清楚返回的数据类型的，因为value的数据类型为any。但我们说尽量不使用any，毕竟它绕过了类型检查，因此，我们需要借助泛型来解决。

### 1、泛型简单使用

一般我们用 `T` 来代表输入和返回的数据类型，直到调用了才明确 `T` 是什么数据类型：

```typescript
function createArr<T>(length: number, value: T): Array<T>{
// 或：function createArr<T>(length: number, value: T): T[]{
    let arr: T[] = [];
    for(var i=0;i<length;i++){
        arr[i] = value;
    }
    return arr;
}

console.log( createArr<string>(3, "猴子的救兵") );  // [ '猴子的救兵', '猴子的救兵', '猴子的救兵' ]
```

### 2、多个类型参数

如果有多个参数都是类型未知的，那么泛型应该这么写：

```typescript
// 情况一：多个参数传入
function fn<U, T>(flag: U, value: T): Array<U | T>{
    let arr: Array<U | T> = [flag, value];
    return arr;
}

console.log(fn(true, "我是孙悟空"))		// [ true, '我是孙悟空' ]

// 情况二：单个参数传入，且为数组
function fn<U, T>(arr: [U, T]): Array<U | T>{
    let newArr: Array<U | T> = [...arr];
    return newArr;
}

console.log(fn([true, "我是孙悟空"]))  // [ true, '我是孙悟空' ]
```
