书籍目录
1- TypeScript介绍
2- TypeScript安装和环境搭建
3- 基础类型
4- 枚举
5- 元组
6- never和unknow
7- 交叉类型和联合类型
8- 接口
9- 函数
10- 类
11- 类型推断
12- 类型断言
13- 类型保护
14- 泛型
15- 装饰器
16- 混入
17- 命名空间
18- 模块
19- 声明文件
665
TypeScript入门教程
限时免费
共19小节
本书从TypeScript的基础知识开始讲解,包括数据类型、变量、运算符、条件语句、循环结构等基本概念。接着,本书详细介绍了TypeScript中的函数、类和接口等高级特性,以及如何使用它们来构建可扩展和可维护的代码。
离线

Mixixi

咨询作者

1-TypeScript介绍

1.1 TypeScript概述

TypeSript 是微软研发的编程语言,并于2012年正式发布,主要用于开发大型应用程序。

TypeScript 的作者是C的首席架构师 安德斯●海尔斯伯格。

TypeScript 是开源和跨平台的编程语言,其开源代码放在了GitHub上,它使用了Apache License 2.0开源协议。

TypeScript 是 JavaScript 的一个超集,支持ECMAScript(ES6)标准。TypeScript 并不能在浏览器上直接运行,不过它可以编译成 JavaScript,然后在浏览器上或者在 node 环境上运行。

TypeScript 有更多的规则和类型限制,其代码具有更高的可控性以及可预测性,并且易于维护和调试。对模块、命名空间和面向对象的支持,更容易组织并开发大型复杂程序。

1.2 语言特性

  • 与 JavaScript 相兼容,TypeScript 是 JavaScript 的扩展,这意味着 JavaScript 的程序都是合法的 TypeScript 程序。

  • TypeScript 是一种静态类型编程语言,它可以在编译期就能发现一些基本的类型错误。

  • 函数多返回值,通过元祖可以使函数具有多返回值。

  • 类型推导,TypeScript 可以根据变量后面的值自动推导出变量的具体类型。

  • 支持最新的 ECMAScript 特性,使用 TypeScript 可以体验到最新的 ECMAScript 特性。

  • 通过编译器选项,可以控制编译过程和输出结果。

1.3 成功案例

VSCode:微软的IDE编辑器就是使用 TypeScript,基于 Electron 框架进行开发。现在VSCode 是世界上最受欢迎的IDE编辑器。

Angular:Angular 是谷歌开源的Web框架,Angular 团队也推荐优先使用 TypeScript 进行开发 Angular 应用

Vue3.0:Vue3.0 使用 TypeScript 进行了重构,并且原生支持 TypeScript。

2-TypeScript安装和环境搭建

这节课程我们主要开发环境以及IDE编辑器的安装等内容,安装好开发环境之后我们就可以正式学习后面的内容。

2.1 TypeScript的安装

TypeScript 是无法直接运行的,它必须通过 Node 环境编译成 js 文件之后才能运行,所以我们需要先安装 Node 才能让 TypeScript 代码正常运行起来。

2.1.1 安装Node环境

在官网 https://node.js.org 下载安装,不过推荐使用LTS版本(长期支持版本)进行安装。

下载并安装

点击官网的 Download。然后根据自己的操作系统选择对应的安装包下载进行安装。

图1 下载Node

还可以直接选择 Node 的历史版本然后下载安装

图2 历史版本 (1)

图3 历史版本 (2)

图4 历史版本 (3)

开始安装

  1. 找到并点击下载好的安装包,点击 Next 开始进行安装。
图5 安装 (1)

  1. 勾选 接受,继续安装。
图6 安装 (2)

  1. 根据自己的喜好,可以将 Node.js 安装到你选择的磁盘目录下,点击 Next

    点击 change 可以选择 Node.js 安装到你选择的磁盘目录。

图7 安装 (3)

图8 安装 (4)

  1. 保持默认,点击 Next
图9 安装 (5)

  1. 继续点击 Next
图10 安装 (6)

  1. 点击 install 开始安装,最后等待进度条结束完成安装。
图11 安装 (7)

  1. 在 Node.js 的安装目录下创建 node_globalnode_cache 文件夹。
图12 安装 (8)

查看node版本:node -v

查看npm版本:npm -v

图13 查看node和npm版本

nodejs 的安装目录下创建 node_globalnode_cache 文件夹。

管理员身份打开cmd,运行下面命令:

npm config set prefix "D:\nodejs\node_global"
npm config set prefix "D:\nodejs\node_cache"

环境变量

Windows 的设置相对比较复杂,这里主要以 Windows 设置为例。

  • Windows 操作系统下,通过 系统 > 高级系统设置 > 环境变量 来进行环境变量设置。

    图14 环境变量 (1)

    图15 环境变量 (2)

  • 在系统变量 path > 编辑 里添加 D:\NodeJS

    图16 环境变量 (3)

    图17 环境变量 (4)

  • 在系统变量中新建 变量名 NODE_PATH,变量值 D:\nodejs\node_global\node_modules

    图18 环境变量 (5)

    然后在系统变量 path > 编辑 里添加 %NODE_PATH%

    图19 环境变量 (图6)

    图20 环境变量 (7)

  • 在用户变量 path > 编辑,新增 D:\NodeJS\node_global

    图21 环境变量 (8)

设置镜像地址

在cmd中输入以下命令:

npm config set registry https://registry.npm.taobao.org

查看是否配置成功,在cmd中输入 npm config get registry 是否能成功输出淘宝的镜像地址。

注意: 如果遇到 npm ERR: request to https://registry.npm.taobao.org failed, reason: certificate has expired错误,这是因为证书已经过期了;淘宝镜像站已经切换成新的域名。需要将 https://registry.npm.taobao.org 换成 https://registry.npmmirror.com

2.1.2 安装TypeScript

全局安装

在开发环境中安装了 Node.js 后就可以使用npm命令来安装 TypeScript。在cmd中输入 npm install typescript -global

输入 tsc -v 查看TypeScript版本。

tsc -v
Version 5.2.2

本地安装

在本地创建一个文件夹 ts_demo,输入 npm install typescript -D 安装TypeScript。

在cmd中输入 ./node\_modules/typescript/bin/tsc -v 查看版本号

ydcq@ydcqdeMac-mini ts_demo % ./node_modules/typescript/bin/tsc -v
ydcq@ydcqdeMac-mini ts_demo % Version 5.2.2

2.1.3 打印Hello World

编写代码

ts_demo 文件夹中创建一个 hello.ts 文件,然后输入以下代码:

function sayHello() {
  let str = "Hello World!"
  console.log(str);
}
    
sayHello();

编译运行

因为在 TypeScript 无法直接运行,所以我们需要在cmd中输入 tsc hello.ts 进行编译,编译完成后生成 hello.js 文件。输入 node hello.js,打印Hello World。

ydcq@ydcqdeMac-mini ts_demo % ./node_modules/typescript/bin/tsc hello.ts
ydcq@ydcqdeMac-mini ts_demo % node hello.js
ydcq@ydcqdeMac-mini ts_demo % Hello World!

2.1.4 ts-node本地运行ts文件

每次都要编译后再运行实在是太麻烦了,不过我们可以使用 ts-node 直接运行 ts 文件。在终端输入 ./node_modules/typescript/bin/tsc --init 命令初始化生成一个 tsconfig.json 配置文件。

在终端中输入 npm install ts-node -D 命令来安装 ts-ndoe

项目下创建 main.ts,然后在 package.json 文件中输入 "dev": "ts-node --files ./main.ts"

{
  "scripts": {
    "dev": "ts-node --files ./main.ts"
  },
  "devDependencies": {
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  }
}

以后只需要运行 npm run dev 就能直接运行ts文件了。

ydcq@ydcqdeMac-mini ts_demo % npm run dev    
    
> dev
> ts-node --files ./main.ts
    
Hello World!
ydcq@ydcqdeMac-mini ts_demo % 

注意:如果 TypeScriptts-node 是全局安装的情况,使用 VSCodeCodeRunner 插件就可以直接运行 ts 代码。

2.2 开发环境搭建

IDE编辑器我们优先使用 VS Code,因为 VS Code 是免费的,它是世界上最受欢迎的编辑器工具。VS Code(全称:Visual Studio Code)是一款由微软开发的编辑器,它具有高亮显示,代码补全等功能,此外插件市场还有非常丰富的扩展支持。

2.2.1 VSCode安装

VS Code 官网https://code.visualstudio.com ,根据自己的操作系统下载对应的安装包。

图22 VS Code官网

双击下载好的安装包并勾选同意按钮,点击下一步进行安装。

图23 开始安装

根据自己的喜好设置安装到磁盘的位置。

图24 选择磁盘位置

勾选对应的选项点击下一步继续安装等待安装。

图25 继续安装

最后点击完成。

图26 点击完成

安装完成后的界面。

图27 安装成功

 

2.2.2 设置简体中文

我们发现刚才安装的 VS Code 还是英文的,接下来我们就要设置它的中文界面。

点击下面的图标,然后输入 chinese,选择第一个,然后点击 install 进行安装。

图28 简体中文插件

完成后根据提示重启 VS Code 就能显示中文了。

图29 简体中文界面

2.2.3 开发和调试

有时候我们需要逐步查看代码中变量的值进行排查错误,这个时候就需要进行调试,

点击下面的图标,点击创建 launch.json 文件,选择第一个 Node.js,接下来就能看到创建好的配置文件。

图30 生成配置文件

接下来点击某一行代码,打上断点,准备调试。

图31 打断点准备调试

点击下面的图标,然后点击调试程序,程序就停到有断点的代码所在行。

图32 调试程序

3-基础类型

TypeScript 支持与 JavaScript 基本一样的数据类型,下面我们介绍 TypeScript 中一些基础类型。注意在写 TypeScript 时,与 JavaScript 不同的地方时需要注意变量的类型。

3.1 布尔

布尔表示真假,是最简单的数据类型,值只有true或者false,在 TypeScript 中叫做 boolean

let isLight: boolean = false;
let isRight = true;
// isRight = 1; // 错误:不能将类型“number”分配给“boolean”类型。
    
console.log(isLight);
console.log(isRight);

注意:变量名后可以接类型,也可以不接;不接类型, TypeScript 就会自动推断出这个变量的数据类型。

3.2 数字

TypeScript 的数字支持二进制、十进制、八进制和十六进制等,数字类型都是以number来表示。

let num = 10;
let binaryLiteral = 0b1010;
let octalLiteral = 0o12;
let hexLiteral = 0xa;
    
console.log(num);
console.log(binaryLiteral);
console.log(octalLiteral);
console.log(hexLiteral);

注意:字面量(literal)表示一个固定的值,它可以理解为没有用标识符封装的量,它是值的原始状态。

3.3 字符串

使用双引号或者单引号来表示字符串,它的数据类型是 string

let str = "hello";
let name = "张三";
let intro = `我叫${name}`;
    
console.log(str);
console.log(intro);

注意:模版字符串是使用反引号来表示,语法:`${表达式}`。模板字面量的表达式只是一个占位符,最终会被替换为实际的值。

3.4 undefined和null

在 TypeScript 里面 undefinednull 也是一种类型,例如:

let variable1: undefined = undefined;
let variable2: null = null;
// 开启strictNullChecks为true时报错
//let num: number = undefined; // 不能将类型“undefined”分配给类型“number”
//let str: string = null; // 不能将类型“null”分配给类型“string”
    
console.log(variable1);
console.log(variable2);
//console.log(num);
//console.log(str);

注意:
1.当 tsconfig.json 中设置了 strictNullCheckstrue 时,就不能把undefined或者null赋值给他们自身和 void 之外的类型。
2.此外还可以使用联合类型,例如:let str: string | undefined = undefined;

3.5 void

void 类型表示没有任何类型;当一个函数没有返回值的时候,那么它的返回值就是 void 类型。

function sayHello(): void {
  console.log("Hello World!")
}
    
sayHello();

注意:
1.当一个变量为 void 类型时,可以将它的值赋值为 undefined 或者 null,例如:

let value1: void = undefined;
// strict为false时报错
//let value2: void = null; // 不能将类型“null”分配给类型“void”

console.log(value1);
//console.log(value2);

2.在 tsconfig.json 里面将 strict 设置为 true,就可以将 null 赋值给 void

3.6 数组

TypeSctiprt 的数组有两种定义方式,第一种就是类型后面接上 [],例如:

let arr: number[] = [1, 2, 3, 4];
let arr2: string[] = ['a', 'b', 'c', 'd'];
    
console.log(arr);
console.log(arr2);

另一种就是使用数组泛型,格式:Array<类型>,例如:

let arr: Array<number> = [1, 2, 3, 4];
let arr2: Array<string> = ['a', 'b', 'c', 'd'];
    
console.log(arr);
console.log(arr2);

3.7 any

当不确定变量到底是什么类型的时候,我们并不希望编译器对这些值进行编译阶段检查。这时候我们就可以使用 any 类型,例如:

let value: any = null;
value = 10;
console.log(value);
    
value = "hello";
console.log(value);

注意:如果一个变量的类型是 any 类型时,就可以随意的访问它的属性(即使不存在这个属性),例如:

let value: any = "zhangsan";
    
console.log(value.name);
console.log(value.age);

3.8 object

object 表示非原始类型,也就是除了 numberstringbooleansymbolundefinednull 之外的类型。object 是对内存地址的引用,是引用类型。

let obj = { name: "张三" };
console.log(obj);
    
// obj的name属性已经发生变化
change(obj, "李四");
console.log(obj);
    
function change(obj: any, value: string) {
  obj.name = value;
}

注意:
1.objectTypeScript 中的类型,它是非原始类型的统称,例如:普通对象、数组、函数等等。
2.ObjectJavaScript 中的对象,它包含 JavaScript 中所有的内置对象,在 JavaScript 中一切引用类型都是对象。

4-枚举

TypeScript 的枚举是对 JavaScript 数据类型的补充,其他语言也有枚举这个类型,它是一组具有数值的成员。定义枚举使用 enum,每个枚举成员用逗号隔开。

4.1 数字枚举

如果枚举成员没有给数字,枚举成员的起始编号默认从0开始,然后依次累加。例如下面例子 Weeks.Monday 打印输出0Weeks.Tuesday 打印输出 1...

enum Weeks {
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}


let week: Weeks = Weeks.Friday;
console.log(week); // 输出4
// 还可以通过key,value的方式获取枚举成员
console.log(Weeks["Friday"]); // 输出4

function weekDesc(value: Weeks): string {
  let res: string = "";
  switch (value) {
    case Weeks.Monday:
      res = "星期一"
      break;
    case Weeks.Tuesday:
      res = "星期二";
      break;
    case Weeks.Wednesday:
      res = "星期三";
      break;
    case Weeks.Thursday:
      res = "星期四"
      break;
    case Weeks.Friday:
      res = "星期五"
      break;
    case Weeks.Saturday:
      res = "星期六"
      break;
    case Weeks.Sunday:
      res = "星期天"
      break;
  }
  return res;
}

console.log(weekDesc(Weeks.Saturday)) // 星期六

如果我们给 Weeks.Monday 的起始编号设置为1,其他成员那就变成从1开始累加,例如:

enum Weeks { 
  Monday = 1,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}
    
    
let week: Weeks = Weeks.Friday;
console.log(week); // 输出5

4.2 字符串枚举

我们还可以将枚举的成员设置为字符串,字符串枚举要求每一个成员都必须是字符串字面量(字符串枚举不具有枚举编号递增,不过它有更好的可读性。),例如:

enum Message { 
  Success = "Operation successful",
  Faild = "Operation failed"
}
    
console.log(Message.Success); // Operation successful

4.3 计算枚举

枚举的成员还可以是计算表达式,例如:

enum MemoryUnit { 
  KB = 1024,
  MB = 1024 * KB,
  GB = 1024 * MB,
  TB = 1024 * GB,
  PB = 1024 * TB
}
    
console.log(MemoryUnit.KB);
console.log(MemoryUnit.MB);
console.log(MemoryUnit.GB);
console.log(MemoryUnit.TB);
console.log(MemoryUnit.PB);

注意:1.计算表达式必须是常量,也就是可以明确知道表达式的具体值。2.枚举值可以是这个枚举的其他成员。

4.4 异构枚举

枚举的成员的枚举值还可以是不同的数据类型,例如:

enum Message {
  OK = 200,
  Failed = "Operation failed"
}

console.log(Message.OK);
console.log(Message.Failed);

如果不是真的需要,不建议使用。因为枚举的特点就是一组相同类型的成员,异构枚举会让代码造成混乱,其他人根本就看不懂。

4.5 反向映射

反向映射就是使用枚举值反向获取枚举的成员,例如:

enum Weeks {
  Monday = 1,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}

console.log(Weeks[1]) // Monday
console.log(Weeks[2]) // Tuesday
console.log(Weeks[3]) // Wednesday

注意:反向映射只适合数字类型枚举,字符串枚举不支持反向映射。

4.6 枚举的遍历

可以使用 for-in 语句对枚举进行遍历,例如:

enum Weeks {
  Monday = 1,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}

for (let week in Weeks) {
  console.log(week)
}

5-元组

TypeScript 的元组和数组有点像,只不过数组里面是相同的类型,而元组里面是不同的类型。

5.1 元组的定义

元组的定义方式是 [数据类型...],例如:

// let arr: string[] = ["A", "B", "C"];
// arr = ["A", "B", 1]; // 数组里面是相同类型

let tuple: [string, number] = ["张三", 18];
console.log(tuple);

注意:
1.any[]类型的数组可以是不同的类型,但是any[]类型的数组的值都是any类型,不知道它的具体的类型。
2.元组的类型是有顺序的,例如:[string, number] 类型的元组,第一个值必须是字符串类型,第二个值必须是数字类型,不能随意改变顺序。

5.2 元组的访问

可以通过索引来访问元组,第一个是0,依次加1…

let tuple: [string, number] = ["张三", 18];
// 访问元组
console.log(tuple[0]);
console.log(tuple[1]);

// 修改元组
tuple[0] = "李四";
console.log(tuple);

此外还可以通过 [变量名1, 变量名2, ...] 的方式来获取元组的值,这种方式叫做解构元组。

let tuple: [string, number] = ["张三", 18];

let [perName, perAge] = tuple;
console.log(perName);
console.log(perAge);

5.3 元组的其他操作

通过 push 方法向元组最后一位添加元素。pop 方法可以移除元组的最后一位元素。

let tuple = ["张三", 18];

// 末尾添加元素
tuple.push("上海");
console.log(tuple)

// 末尾移除元素
tuple.pop();
console.log(tuple)

6-never和unknow

在 TypeScript 中,neverunknown 是两种不同的类型。下面就让我们介绍它们的区别和用法。

6.1 nerver

nerver 类型表示那些永远不存在的值的类型。例如,nerver 类型是那些总会抛出异常或者根本不会有返回值的函数。nerver 类型是任何类型的子类型,它可以赋值给任何类型。同时也没有任何类型是 never 类型的子类型或者可以赋值给 never

// 永远不会有返回值类型的函数
function getStudentName(): never {
    throw new Error("error");
}

// 死循环
function getStudentName2(): never {
  while (true) {

  }
}

6.2 unknow

unknowany 类型的安全版;any类型编译时不会进行类型检查,而 unknow 类型则要求必须能确定类型,否则不能进行操作。也就是说 unknow 不能保证变量的类型,但是却能保证该变量安全。

let student: unknown = { name: "张三", age: 18 };

// 无法访问name和age
// console.log(stu.name);
// console.log(stu.age);

// 操作unknown类型之前,必须指定它的类型
let stu = student as { name: string, age: number };
if (stu != null) {
  console.log(stu.name);
  console.log(stu.age);
}

7-交叉类型和联合类型

在 TypeScript 中,交叉类型(Intersection Types)和联合类型(Union Types)是用来组合多个类型的方式。

7.1 交叉类型

交叉类型表示多个类型的合并,多个类型之间使用 & 符号连接。

let obj1: { name: string, age: number } = { name: "张三", age: 18 }
let obj2: { address: string } = { address: "上海" }
console.log(obj1);
console.log(obj2);
    
// obj既有name和age属性,也有address属性
let obj: { name: string, age: number } & { address: string } = { name: "李四", age: 18, address: "杭州" }
console.log(obj);

7.2 联合类型

联合类型表示取值是多个类型中的某一种,多个类型之间用 | 符号连接。当取值具有多样性,元素的类型不唯一,就可以使用联合类型。

let message: number | string;
    
message = 200;
console.log(message);
    
message = "success";
console.log(message);

8-接口

在 TypeScript 中,接口(Interface)用于定义对象的结构和类型。通过接口,我们可以明确定义对象应该包含哪些属性以及它们的类型。

8.1 接口的声明和使用

接口是对象的状态和行为的抽象,接口就像协议或者一组规范,是用来对对象的属性和方法进行约束的。接口的定义如下:

interface 接口名称 {

  // 属性...

  // 方法...

}

下面是定义接口的例子:

interface Animals {
  name: string;
  age: number;
}

let cat: Animals = { name: "猫", age: 1 };
let dog: Animals = { name: "狗", age: 3 };
console.log(cat);
console.log(dog);

8.2 索引类型

可索引类型的接口只有两种,即索引值为 string 或者 number

interface IndexObj {
  [index: number]: string
}

let books: IndexObj = {
  0: "HTML",
  1: "Javascript",
  2: "php",
  3: "python"
}
console.log(books[1]);

8.3 可选属性

接口的属性有时候并不是必须的,或者说它有时候允许不存在,这个时候可以把这个属性定为可选属性。定义方式是在属性名称后面加一个?。

interface Animals {
  name: string;
  age: number;
  weight?: number;
}

let cat: Animals = { name: "猫", age: 1 };
let dog: Animals = { name: "狗", age: 3, weight: 10 };
console.log(cat);
console.log(dog);

8.4 只读属性

有些属性只允许在创建时赋值,之后就不允许修改。只需要在属性名前面加上 readonly 就变为只读属性。

interface Student {
  readonly no: number;
  name: string;
  score: number
}

let student: Student = { no: 10000, name: "张三", score: 100 };
//student.no = 10001; // 报错,不允许修改
console.log(student);

8.5 接口继承

在 TypeScript 中,接口之间可以建立继承关系,以便复用已有的接口定义并添加新的属性或方法。

8.5.1 单继承

接口也可以继承接口用来扩展自己,继承使用 extends 关键字。

语法:接口1: extends 接口2

interface Animal {
  name: string;
  weight: number;
  age: number;
}

// Person继承自Animal
interface Person extends Animal {
  address: string;
}

let person: Person = { name: "张三", weight: 60, age: 18, address: "上海" };
console.log(person);

8.5.2 多继承

接口可以多继承,只需要将多个接口使用逗号,隔开。

语法:接口: 接口1, 接口2...

interface Animal {
  name: string;
  weight: number;
  age: number;
}

// Person继承自Animal
interface Person extends Animal {
  address: string;
}

// Student继承自Animal和Person
interface Student extends Animal, Person {
  readonly no: number;
  score: number
}

let student: Student = { no: 10000, name: "张三", age: 18, weight: 60, address: "上海", score: 100 };
//student.no = 10001; // 报错,不允许修改
console.log(student);

8.6 interface和type的区别

interfacetype 都可以定义对象类型,那么开始时该如何选择呢?

如果是定义非对象类型,通常建议使用 type,比如:Direction、Alignment和一些Function等。

如果是定义对象类型 interfacetype 就有区别了。

  • inteface 可以重复对某个接口进行定义属性和方法。

  • type 定义的是别名,不允许重复定义。

interface Person {
  name: string;
}

/* 接口可以重复定义属性,因为 TypeScript 会把这两个一样的接口进行合并,结果相当于是
interface Person {
  name: string;
  age: number;
  talk: () => void;
}
 */
interface Person {
  age: number;
  talk: () => void;
}

let person: Person = {
  name: "张三", age: 18, talk () {
    console.log("hello, 我是张三");
  },
}
console.log(person);
person.talk();

type Student = { no: number, name: string };
// type Student = { score: number }; // 报错
let student: Student = { no: 10000, name: "张三" };
console.log(student);

9-函数

函数是程序中用于实现特定任务或操作的一段可重复使用的代码块。

9.1 函数的定义

函数包含方法名,参数名,参数类型和返回值等,它的定义格式:

function 方法名(参数1:参数类型, 参数2: 参数类型...): 返回值类型 { 

  函数体

  return 返回值;
}

下面是一个函数的例子:

function sum(num1: number, num2: number): number {
  return num1 + num2;
}

console.log(sum(1, 2));

9.2 接口函数

接口除了可以描述对象类型外,还可以描述函数类型。

interface Add {
  (x: number, y: number): number;
}

const add: Add = (x: number, y: number) => x + y;
console.log(add(1, 2));

9.3 类型别名函数

使用 type 类型别名也可以定义函数类型。

type Add = (x: number, y: number) => number;

const add: Add = (x: number, y: number) => x + y
console.log(add(1, 2));

9.4 可选参数

函数的参数有时也不是必须的,这时候只需要在函数的参数名前加 ? 符号,就可以实现可选参数。

function getResult(code: number, errMsg?: string) {
  switch (code) {
    case 200:
      console.log("成功");
      break;
    default:
      if (errMsg != null) {
        console.log(errMsg);
      } else {
        console.log("失败");
      }
      break;
  }
}

getResult(200);
getResult(500, "服务端错误");

9.5 默认参数

如果函数的参数如果默认情况下都是固定的,只在一些特殊情况才会改变,就可以使用默认参数。这样在调用函数的时候,该参数就可以不必直接给定实际参数。

function getUrl(path: string, host: string = "http://127.0.0.1"): string {
  return host + path;
}

console.log(getUrl("/api/login"));
console.log(getUrl("/api/login", "http://192.168.0.192"));

注意:1.默认参数的数据类型可以不写。2.默认参数一般都是放在最后面。3.如果除了默认参数其它参数都是可选型,那么默认参数可以放在第一位。

9.6 剩余参数

有的时候我们传入的参数可能是多个,或者说不知道是多少个,这时候就应该使用剩余参数。剩余参数会被当作是个数不确定的可选参数,可以一个也没有,也可以是任意多个。

function getFilterPathString(...params: string[]): string {
  return params.join(",");
}

console.log(getFilterPathString("/api/login", "/api/register", "/api/userAgreement", "api/privacyPolicy"));

9.7 函数多返回值

一般情况下,函数只能有一个返回值,但是有些情况下,我们的函数需要返回多个值,就只能封装成对象,或者放到数组里面。在调用的时候再拿出来,那么有没有办法让函数直接返回多个返回值,直接就拿来使用呢?还真有,我们可以使用元祖让函数有多个返回值,来看看下面的例子。

function getMaxValueAndMinValue(arr: number[]): [number, number] {
  let max: number = arr[0];
  let min: number = arr[0];
  for (let index = 1; index < arr.length; index++) {
    const element = arr[index];
    // 获取最大值
    if (element > max) {
      max = element;
    }

    // 获取最小值
    if (element < min) {
      min = element;
    }
  }

  return [max, min];
}

let [max, min] = getMaxValueAndMinValue([12, 9, 32, 24, 56, 28]);
console.log("max=", max);
console.log("min=", min);

9.8 函数重载

函数的重载指的是函数名相同,而形参不同的多个函数。

TypeScript 函数重载的语法如下:

function 函数名(参数1: 类型1, 参数2: 类型2): 返回值类型;
function 函数名(参数: 类型): 返回值类型;
// 第一步,定义函数签名
function add (x: number, y: number): number;
function add (x: string, y: string): string;
// 第二步,实现函数重载的函数签名
function add (x: any, y: any): any {
    if (typeof x == "string" && typeof y == "string") {
        return x + y;
    } else {
        return Number(x) + Number(y);
    }
}
console.log(add(1, 2));
console.log(add("1", "a"));

function desc(name: string, age: number): string;
function desc(name: string, age: number, address: string): string;
function desc(name: string, age: number, address?: string): string {
  let msg: string = "我叫" + name + ", 今年" + age + "岁";
  if (address != null) {
    msg += ", 家住" + address;
  }
  return msg;
}
console.log(desc("张三", 18));
console.log(desc("李四", 20, "上海"));

注意:
1.定义函数重载需要定义重载签名函数和一个实现签名的函数。
2.重载签名函数没有函数体,一个实现签名的函数可以有多个重载签名函数。

10-类

JavaScript 自ES6有了类的特性,而 TypeScript 的类是在ES6中的类上进行了扩展。TypeScript 的类可以继承接口,还有了publicprivateproteced等访问修饰符和静态属性和方法等。

10.1 构造函数

类的构造方法是类在实例化时调用,可以为类的对象分配内存,定义构造函数使用 constructor 关键字。

class Person {
  name: string;
  age: number;
  weight?: number = 50;

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

  intro(): void {
    console.log("我叫" + this.name, " ,今年" + this.age + "岁");
  }
};

//let person: Person = new Person("张三", 18);
let person: Person = new Person("张三", 18, 50);
console.log(person);

注意:
1.类自动带有不带参数的构造函数。
2.如果类中已经有了带有参数的构造方法,那么就不能再使用不带参数的构造函数。
3.this指的是这个类具体的实例对象。
4.创建对象使用new关键字。
5.构造方法也可以重载。
6.构造方法的实现只能有一个。
7.类的属性和方法也可以设置默认值。

10.2 readonly

也可以使用 readonly 修饰类的属性,表示该属性是只读的,只读属性只能在定义时赋值或者在构造函数中被赋值。

class Circle {
  readonly pi: number = 3.14;
  readonly radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  get circumference(): number {
    return 2 * this.pi * this.radius;
  }
}

const circle = new Circle(10);
console.log(circle.circumference); // 输出31.4
//circle.pi = 3.1415; // 报错,因为pi是只读属性

10.3 类的继承

在 TypeScript 里我们可以使用面向对象的编程模式,使用类的继承可以用来扩展现有的类。继承使用 extends 关键字。

class Animal {
  constructor() {

  }
}

class Person extends Animal {
  name: string;
  age: number;

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

  intro(): void {
    console.log("我叫" + this.name, " ,今年" + this.age + "岁");
  }
};

class Student extends Person {
  score: number;
  weight?: number;

  constructor(name: string, age: number, score: number);
  constructor(name: string, age: number, score: number, weight: number);
  constructor(name: string, age: number, score: number, weight?: number) {
    super(name, age);
    this.score = score;
    this.weight = weight;
  }

  studay(): void {
    console.log("好好学习,天天向上。")
  }
}

// let student: Student = new Student("张三", 18, 100);
let student: Student = new Student("张三", 18, 100, 50);
console.log(student.name);
console.log(student.age);
student.intro();
student.studay();

注意:
1.使用super可以调用父类的属性和方法。
2.非私有的属性和方法可以被子类继承。
3.被继承的类叫做父类(超类),继承的类叫做子类(派生类)。
4.如果父类有构造函数,子类在构造函数中要调用父类的构造函数。
5.子类可以重写父类的方法,子类重写过后的方法会被优先调用。
6.子类调用父类的构造函数,super必须放在第一句。
7.继承的缺点是增强了代码的耦合。优点是扩展现有类,提高代码的复用。

10.4 访问控制符

访问修饰符指的是用来描述该类的内部属性和方法的可访问性。

修饰符 描述
public 默认值,完全公开,外部可以访问。
private 只有类的内部才可以访问。
proteced 该类的内部和子类才能访问。

10.4.1 static和instanceof

static

static 修饰的属性和方法被称为静态成员变量和静态方法。这些静态成员变量和静态方法属于类,不属于对象,只能通过类去调用。

class Animal {
  constructor() {

  }
}

class Person extends Animal {
  name: string;
  age: number;
  static weight: number = 50;

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

  intro(): void {
    console.log("我叫" + this.name, " ,今年" + this.age + "岁");
  }

  static getWeight(): number {
    return Person.weight;
  }
};

//let person: Person = new Person("张三", 18);
//person.getWeight(); // 报错
console.log(Person.getWeight());

注意:静态属性是类的属性,非静态属性才是类的实例对象的属性。

静态块是 static 修饰的代码块,静态块在类类的实例化之前调用,并且只会被执行一次。

class Person {
  name: string;
  age: number;

  static {
    console.log("-----------> 静态块")
  }
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
};

let person: Person = new Person("张三", 18);
let person2: Person = new Person("李四", 18);
console.log(person.name);
console.log(person2.name);

静态块一般被用做加载一些配置文件或者读取本地一些不变的文件等等。

instanceof

使用 instanceof 关键字可以判断每个实例对象的所属类,语法是 对象 instanceof 类,结果返回一个 boolean 值。

class Animal {
  constructor() {

  }
}

class Person extends Animal {
  name: string;
  age: number;

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

  intro(): void {
    console.log("我叫" + this.name, " ,今年" + this.age + "岁");
  }
};

class Student extends Person {
  score: number;

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

  studay(): void {
    console.log("好好学习,天天向上。")
  }
}

let person: Animal = new Person("张三", 18);
let student: Animal = new Student("张三", 18, 100);
console.log(person instanceof Person); // true
console.log(student instanceof Person); // true,因为Student继承Person
console.log(student instanceof Student); // true

10.5 set和get方法

通过使用 settersgetters 可以来截取对象成员变量的访问,还可以保护类的数据的安全性。

class Animal {
  constructor() {

  }
}

class Person extends Animal {
  name: string;
  private _age: number = 0;

  constructor(name: string) {
    super();
    this.name = name;
  }

    set age(value: number) {
      if (value <= 0 || value > 200) {
        throw new Error("没有人能超过200岁");
      }
      this._age = value;
    }

    get age() {
      return this._age;
    }
};

let person: Person = new Person("张三");
// person.age = 300; // 报错
person.age = 18; // 调用setters方法修改属性
console.log(person.age);

10.6 类和接口

类可以实现接口,实现某个接口使用 implements 关键字。

interface IIntro {
  intro (): void;
}

class Person implements IIntro {
  name: string;
  age: number;

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

  intro(): void {
    console.log("我叫" + this.name, " ,今年" + this.age + "岁");
  }
};

let person: Person = new Person("张三", 18);
person.intro();

10.7 抽象类

抽象类是作为其他派生类的基类来使用,他们一般不能被实例化。

抽象类一般是用来定义那些不希望被外界直接所创建的类或者是最顶层的类,因为一般越是顶层的类也就越是抽象。

定义抽象类使用 abstract 关键字。

abstract class Animal {
  //abstract run(): void;
  run(): void {
    console.log("疯狂的跑")
  }
}

class Person extends Animal {
  name: string;
  age: number;

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

//let animal: Animal = new Animal(); // 报错
let person: Person = new Person("张三", 18);
person.run();

注意:抽象类和接口一样用来约束或者规范类,只不过接口只定义,不实现。

11-类型推断

类型推断(Type Inference)是 TypeScript 的一个重要特性,它能够根据代码的上下文自动推导变量和表达式的类型,而无需显式地指定类型。

11.1 基础类型推断

在定义变量的时候如果没有明确指定数据类型,TypeSscript 可以自动推断变量的类型。

  1. 初始化变量

    let num = 10;
    //num = "10"; // 报错,不能将类型“string”分配给类型“number”
    console.log(num);
    
  2. 函数的参数默认值

    // message被推断为string类型
    function printMessage(message = "hello") {
      console.log(message);
    }
    
    printMessage("Hello World!");
    //printMessage(123); // 报错,类型“number”的参数不能赋给类型“string”的参数
    
  3. 函数返回值

    // 返回值推断为number类型
    function add(x: number, y = 10) {
      return x + y;
    }
    
    // 第二个参数有默认值,可以不填写
    let res = add(10);
    console.log(res); // 20
    //res = "100"; // 报错,不能将类型“string”分配给类型“number”
    
  4. 成员变量

    class Person {
      name: string;
      age = 18;
      constructor(name: string) {
        this.name = name;
      }
    }
    
    let person = new Person("张三");
    //person.age = "18"; // 报错,不能将类型“string”分配给类型“number”
    console.log(person);
    

11.2 最佳通用类型推断

当从多个表达式中推断出一个类型的时候,TypeScript 会尽可能的推断出一个最佳的通用类型。

let value = ["张三", 100];

上面例子的 value 就被推断成 string|number 类型。

11.3 上下文类型推断

前面的类型推断都是从右侧向左侧进行推断,还有一种情况就是从左向右的上下文类型推断。

window.onkeydown = (event) => {  // KeyboardEvent
  // event会根据window.onkeydown被推断为KeyboardEvent类型
  console.log(event);
}

12-类型断言

类型断言主要用于当 TypeScript 推断出来的类型并不满足你的需求,你需要手动去指定一个类型。

类型断言主要有 as<> 两种方式:

12.1 使用as语法

使用as关键字断言去覆盖TypeScript推断的类型。

interface Person {
  name: string;
  age: number;
}

// let person = {}; // 直接定义对象没有name和age属性
let person = {} as Person;
person.name = "张三";
person.age = 18;
console.log(person);

12.2 使用尖括号<>语法

另外,类型断言还可以使用<>来实现。例如:

interface Person {
  name: string;
  age: number;
}

let person = <Person>{};
// let person = {}; // 直接定义对象没有name和age属性
person.name = "张三";
person.age = 18;
console.log(person)

注意:
1.<>语法需要放在数据之前来使用。
2.<>语法会和JSX语法起冲突,所以一般使用as语法。

12.3 const断言

const 断言是将宽泛的类型它能推断出来的最窄的类型或更具体的类型。

let num = 100 as const;
// num = 20; // 报错,不能将类型“20”分配给类型“100”
console.log(num);

let str = "hello" as const;
// str = "100"; // 不能将类型“"100"”分配给类型“"hello"”
console.log(str);

let arr = [1, 2, 3, 4] as const;
// arr[0] = 100; // 报错,无法为“0”赋值,因为它是只读属性
console.log(arr);

let person = { name: "张三", age: 18 } as const;
// person.name = "李四"; // 报错,无法为“name”赋值,因为它是只读属性
console.log(person);

注意:
1.原始类型会被推断成具体的值类型。
2.数组会被推断成readonly元祖;对象类型会被推断成readonly属性。

12.4 非空断言

非空断言用于在不进行任何检查的情况下,从类型中删除 nullundefined。语法是在表达式后面加上感叹号!。

let num: number | null = null;
    
// num.toFixed(2); // 报错,“num”可能为 “null”
num!.toFixed(2); // 使用非空断言为number类型,所以可以使用toFixed方法

注意:非空断言只会让编译期不显示的报错,不然实际运行还是会出现错误,所以你必须保证这个变量实际是有值的,非空的,这个时候才能安全的使用非空断言。

12.5 双重断言

既然任何类型都可以被断言为 any 或者 unknown 类型,那么再断言成一个具体的类型。例如

interface Person {
  name: string;
  age: number;
}

let person: Person = "张三" as any as Person; // 断言失败
console.log(person);

注意:
1.尽量不要使用双重断言,因为它会破坏原有的类型关系。
2.双重断言必须保证子类被断言为父类的类型关系。
3.any类型可以被断言为除了never之外的所有类型。

13-类型保护

类型保护通常用于在运行时检查变量的类型,以便能够正确的处理变量。TypeScript 有 typeofinstanceofin以及自定义类型谓词函数等多种方式。

13.1 instanceof

instanceof 用来保护并判断一个对象是否是另一个类的实例,它通常用于引用类型。

class Person {
  name: string;
  age: number;

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

let person = new Person("张三", 18);
console.log(person instanceof Person); // true

let person2 = { name: "张三", age: 18 };
// person2不是Person new出来的对象
console.log(person2 instanceof Person); // false

13.2 typeof

typeof 用来判断元素是否原始类型,返回一个字符串类型。

let num = 100;
console.log(typeof num); // number
    
let str = "hello";
console.log(typeof str); // string
    
let isCheck = true;
console.log(typeof isCheck); // boolean

13.3 in

in 操作符用于判断某个属性是否存在于某个对象中。

interface Person {
  name: string;
  age: number;
}

let person: Person = { name: "张三", age: 18 };
console.log("name" in person); // true
console.log("age" in person); // true

13.4 自定义类型谓词函数

自定义保护类型是利用函数的返回值来判断一个元素的类型。例如:

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function execute(input: unknown) {
  if (isString(input)) {
    console.log("这个变量是string类型")
  } else {
    console.log("这个变量不是string类型")
  }
}

execute("hello");
execute(1234);

value is string 其实就是自定义类型谓词,通过自定义类型谓词函数,我们可以更加灵活的创建安全的代码。

14-泛型

泛型指的是在定义函数,接口后者类的时候,不确定他们具体的类型,先暂时定义一个标记,随后在具体使用的时候再将原来的标记擦除,换成具体的类型。

14.1 泛型的引入

下面我们来看一个不使用泛型的例子:

function createArray(value: any, count: number): any[] {
  let arr: any[] = [];
  for (let index = 0; index < arr.length; index++) {
    arr.push(value)
  }

  return arr;
}

console.log(createArray(10, 5));
console.log(createArray("100", 5));

如果不使用泛型的话,数据类型全部都变成了 any 类型,根本不知道返回值的具体类型。

14.2 泛型函数

定义函数泛型的时候使用 <标记>,之后函数就可以使用这个暂时的类型,尖括号的位置就是最后泛型传入的位置。传入泛型的时候再把具体的类型传入这个函数。

function createArray<T>(value: T, count: number): T[] {
  let arr: T[] = [];
  for (let index = 0; index < arr.length; index++) {
    arr.push(value)
  }

  return arr;
}

console.log(createArray<number>(10, 5)); // 返回 number[]类型
console.log(createArray<string>("100", 5)); // 返回 string[]类型

14.3 多个泛型参数

函数里面可以定义多个泛型类型,然后在函数里面就可以使用多个泛型参数。例如:

function getMessage<T, U>(value: T, message: U): [T, U] {
  return [value, message];
}

console.log(getMessage<number, string>(200, "请求成功"));
console.log(getMessage<number, string>(404, "网页不存在"));

14.4 泛型接口

在定义接口的时候,可以为接口的属性或者方法定义泛型类型,在使用的时候再传入具体的类型。例如:

interface IDatabase<T> {
  queryAllUser (): T[];
  add(data: T): void;
  update(data: T): void;
  delete(data: T): void;
}

class User {
  id: number;
  name: string
  age: number;

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

class UserService implements IDatabase<User> {
  data: User[] = [];

  queryAllUser(): User[] {
    return this.data;
  }
  add(data: User) {
    this.data.push(data);
  }
  update(data: User) {
    for (let index = 0; index < this.data.length; index++) {
      const element = this.data[index];
      if (element.id == data.id) {
        this.data[index] = data;
        return;
      }
    }
  }
  delete(data: User) {
    for (let index = 0; index < this.data.length; index++) {
      const element = this.data[index];
      if (element.id == data.id) {
        this.data.splice(index, 2);
        return;
      }
    }
  }
}

还有一种泛型接口的定义方式

interface MyData {
  <T> (value: T): T;
}

let myData: MyData = function getData<T>(value: T) {
  return value;
}

let data = myData<number>(10000);
console.log()

14.5 泛型类

泛型类和泛型接口比较类似,例如:

class ResultData<T> {
  status: number;
  message: string;
  data: T;

  constructor(status: number, message: string, data: T) {
    this.status = status;
    this.message = message;
    this.data = data;
  }
}

let res = new ResultData<number[]>(200, "请求成功", [1, 2, 3, 4]); // 可以在调用的时候,自己指定类型
console.log(res.data);

这样我们拿到的data的值就是传入的具体类型,不再是原来的any类型。

14.6 泛型约束

如果对泛型类型取一个不存在的属性就会报错,这个时候就可以使用泛型约束来约束它的范围,这样就能拿到想要的属性了。

interface ILength {
  length: number;
}

function execute<T extends ILength>(data: T) {
  console.log(data.length)
}

console.log(execute("hello"));
//console.log(execute(123); // 报错, 类型“number”的参数不能赋给类型“ILength”的参数

15-装饰器

装饰器是用来装饰类、函数、访问器(setters, getters)、属性以及方法的参数等。装饰器提供了一种声明性的方式来修改代码,而不需要修改原始的类或函数定义。

15.1 基本语法

装饰器的基本语法:@expressionexpression 表达式必须是一个函数,这个函数在程序初始化时候被调用,被装饰的信息会作为参数传入。

装饰器是试验性功能,要启用装饰器需要在 tsconfig.json 里面启用 "experimentalDecorators": true 编译器选项。

{
  "compilerOptions": {
	"target": "ES5",
	"experimentalDecorators": true
  }
}

下面是装饰器的例子:

// 这里的myDecorator就是expression,必须是一个函数
function myDecorator(target: any) {
  // 对target做处理
}

@myDecorator
class MyClass {
  // 类的实现
}

target就是要扩展的数据,可以是类、方法、属性、参数等。

15.2 类装饰器

类的装饰器作用于类,用来修改患者替换类的定义。类装饰器可以接收一个参数,这个参数指定要装饰的类。

在类上添加age属性:

function addAge(constructor: any) {
  constructor.prototype.age = 18;
}

//给类添加属性
@addAge
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

let user = new User("张三");
console.log(user.name);
console.log((user as any).age);

装饰器作为装饰类的时候,会将构造器传进去 target.prototype.age 就是在实例化对象的时候添加一个 name 属性。

15.3 装饰器工厂

装饰器工厂本身是一个函数,它可以接收参数并返回一个装饰器函数。这使得我们可以在创建装饰器时传递参数,根据不同的参数返回不同的装饰器。

上面的例子的值是写死的,通过装饰器工厂,可以将值作为参数传进去。

function addAgeFactory(value: number) {
  return function addAge (constructor: any) {
    constructor.prototype.age = value;
  }
}

@addAgeFactory(20)
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

let user = new User("张三");
console.log(user.name);
console.log((user as any).age);

15.4 装饰器扩展类

使用装饰器还可以对类进行扩展,例如:

function extensionUser(constructor: any) {
  // 返回一个继承自constructor的类
  return class extends constructor {
    // 扩展user的属性
    public age = 18;
    public address = "上海";

    // 重载函数
    intro() {
      console.log("我是" + this.name + ", 住在" + this.address);
    }
  }
}

@extensionUser
class User {
  name?: string;

  constructor(name: string) {
    this.name = name;
  }

  intro() {
    //console.log("我是" + this.name);
  }
}

let user = new User("张三");
console.log(user.name);
console.log((user as any).intro());

15.5 访问器装饰器

访问器装饰器是一种用于装饰类的 gettersetter 方法的装饰器。它们可以用于拦截属性的读取和写入操作,并在执行这些操作前后添加额外的逻辑。访问器装饰器获得的参数有三个:

  • target:类或类的原型对象。
  • prop:成员名。
  • descriptor:成员的描述符。
function log (target: any, propKey: string, descriptor) {
  const getter = descriptor.get;
  const setter = descriptor.set;

  if (getter) {
    descriptor.get = function () {
      console.log(`Getting value of ${propKey}...`);
      return getter.call(this);
    };
  }

  if (setter) {
    descriptor.set = function (value: any) {
      console.log(`Setting value of ${propKey} to ${value}...`);
      setter.call(this, value);
    };
  }
}

class User {
  private _name: string;

  @log
  get name() {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

let user = new User();
user.name = "张三";
console.log(user.name);

15.6 属性装饰器

属性装饰器在一个属性声明之前,属性装饰器在程序运行时被调用,传入两个参数:

  • target:被装饰的类的原型。
  • propertyName:被装饰的属性的名称。
function setDefaultValue(value: string) {
  return function (target: any, propertyKe: string) {
	target[propertyKe] = value;
  }
}
    
class User {
  @setDefaultValue("张三")
  name: string | undefined;

  constructor() {}

  intro() {
	//console.log("我是" + this.name);
  }
}

let user = new User();
console.log(user.name);

15.7 方法参数装饰器

方法参数装饰器声明在一个参数之前,方法参数装饰器是一个普通的函数,它可以接收三个参数:

  • target:被装饰的函数或方法的原型(对于静态成员,则是构造函数本身)。
  • methodName:被装饰的方法的名称。
  • parameterIndex:被装饰的参数在参数列表中的索引。
function getParams(target: any, methodName: string, parameterIndex: number) {
  console.log(`target: ${target} 方法名: ${methodName}, 参数索引: ${parameterIndex} 被装饰了`);
}
    
class User {
  //name?: string;
  //age?: number;

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

  getInfo(@getParams name: string, @getParams age: number) {
	return `${name}${age}岁了`
  }
}

let user = new User();
console.log(user.getInfo("张三", 18))

15.8 装饰器执行顺序

TypeScript 装饰器的执行顺序可以使用以下几个规则来理解:

  1. 在一个目标上,多个装饰器的执行顺序是从外到内、从下往上、从右往左的顺序。也就是说,最后一个装饰器首先执行,然后是倒数第二个,以此类推,直到第一个装饰器执行完毕。

  2. 对于类装饰器,它们会在类实例化之前执行。这意味着装饰器可以影响类的构造函数和静态成员,但无法直接访问类的实例成员。

  3. 对于方法、访问器和属性装饰器,它们会在对应成员被定义时立即执行。这意味着装饰器可以用来修改方法的行为、重写访问器的逻辑或者修改属性的元数据。

  4. 对于参数装饰器,从上到下依次执行。与类装饰器类似,最上面的装饰器会先于其他装饰器执行。

function firstClassDecorator(target: any) {
  console.log("Executing firstClassDecorator");
}

function secondClassDecorator(target: any) {
  console.log("Executing secondClassDecorator");
}

function propDecorator(target: any, propertyKe: string) {
  console.log("Executing propDecorator");
}

function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing methodDecorator");
}

function getParams(target: any, methodName: string, parameterIndex: number) {
  console.log(`target: ${target} 方法名: ${methodName}, 参数索引: ${parameterIndex} 被装饰了`);
}


@firstClassDecorator
@secondClassDecorator
class User {
  @propDecorator
  weight: number = 50;
	
  @methodDecorator
  public intro(@getParams name: string, @getParams age: number) {
	return `${name}${age}岁了`
  }
}

const user = new User();
user.intro("张三", 18);

输出结果:

Executing propDecorator
target: [object Object] 方法名: intro, 参数索引: 1 被装饰了
target: [object Object] 方法名: intro, 参数索引: 0 被装饰了
Executing methodDecorator
Executing secondClassDecorator
Executing firstClassDecorator

总结起来,装饰器的执行顺序如下:

  1. 参数装饰器:从上到下、从左到右的顺序执行。
  2. 属性装饰器、方法装饰器:从上到下执行。
  3. 类装饰器:从下到上、从右到左的顺序执行。

16-混入

混入(Mixins) 是面向对象比较重要的概念,根据功能定义多个可复用的 mixins 类。通过 implements 连接多个 mixins,使子类可以根据功能继承需要的 mixins

16.1 对象的混入

使用 ES6 语法的 Object.assign 可以合并多个对象,这个合并后的对象会在 TypeScript 被推断为一个交叉类型的变量。

interface IdataA {
  name: string;
  age: number;
}

interface IDataB {
  address: string;
}

let d1: IdataA = { name: "张三", age: 18 };
let d2: IDataB = { address: "上海" };

let data = Object.assign(d1, d2);
console.log(data);

16.2 类的混入

将多个类作为接口使用 implements 连接来实现这几个 mixins,这样就创建出了一个新的类,而且这个新的类同时具备多个 mixins 的所有功能。

class DataA {
  name: string = "张三";
  age: number = 18;

  getName(): string {
    return this.name;
  }
  getAge(): number {
    return this.age;
  }
}

class DataB {
  address: string = "上海";

  getAddress(): string {
    return this.address;
  }
}

class User implements DataA, DataB {
  address: string;
  name: string;
  age: number;

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


  getName(): string {
    return this.name;
  }
  getAge(): number {
    return this.age;
  }

  getAddress(): string {
    return this.address;
  }
}

let user = new User("张三", 18, "上海");
console.log(user.getName());
console.log(user.getAge());
console.log(user.getAddress());

最后就是借助创建帮助函数来实现混入操作。

这个帮助函数会遍历 mixins 上所有的属性,并复制到目标上去,这样就把之前的占位属性替换成真正实现的代码。

class DataA {
  name: string = "张三";
  age: number = 18;

  getName(): string {
    return this.name;
  }
  getAge(): number {
    return this.age;
  }
}

class DataB {
  address: string = "上海";

  getAddress(): string {
    return this.address;
  }
}

class User { }

function applyMixins(tragetClass: any, originClass: any[]) {
  originClass.forEach(originClass => {
    Object.getOwnPropertyNames(originClass.prototype).forEach(name => {
      tragetClass.prototype[name] = originClass.prototype[name]
    })
  })
}

// 这样就可以实现多个类的混入
applyMixins(User, [DataA, DataB]);

提示:Object.getOwnPropertyNames() 该方法是对某个对象下所有属性进行遍历,该对象指的是数组,遍历的是所有属性名,并只保留该对象自身的属性,除去该对象继承来的属性。

17-命名空间

命名空间是 TypeScript 中用来组织代码的方式之一,类似于其他语言中的模块或者命名空间等。命名空间的定义如下:

namespace MyNamespace {
  // 定义代码
}

在命名空间中定义的变量、函数、类等默认都是私有的,如果你想访问它们,就需要使用 export 关键字将这个 namespace 导出。

namespace Valicate {
  export function validateMobile(input: string): boolean {
    return /^1[34578][0-9]{9}$/.test(input)
  };
}

console.log(Valicate.validateMobile("hello"));

17.1 嵌套命名空间

命名空间支持嵌套,可以将命名空间定义在另外一个命名空间里头,形成层级关系,例如:

namespace ParentNamespace {
  export namespace SubNamespace {
    export class Person {
      name: string;
      age: number;

      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
      sayHello() {
        console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
      }
    }
  }
}

let person = new ParentNamespace.SubNamespace.Person('张三', 18);
person.sayHello();

17.2 命名空间的别名

对于深度嵌套的命名空间,使用命名空间别名非常方便。

namespace ParentNamespace {
  export namespace SubNamespace {
    export class Person {
      name: string;
      age: number;

      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
      sayHello() {
        console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
      }
    }
  }
}

import childNamespace = ParentNamespace.SubNamespace;
let person = new childNamespace.Person('张三', 18);
person.sayHello();

18-模块

模块其实就是一个作用域,它不属于全局作用域。模块内部的变量、函数、类等只在模块内部可见,外部想要使用就必须先使用 export 声明导出模块;然后在其他文件要使用模块就必须使用 import 来导入模块。

18.1 全局模块

在一个 TypeScript 工程创建一个 test1.ts 文件,写入代码:

let a = "hello";

然后,在相同的工程下创建另一个 test2.ts 文件,写入代码:

let a = "world"; // 无法重新声明块范围变量“a”

这时编译器会报错,这是因为它们处在同一全局空间,如果加上 export 导出就不会再报错了。

export let a = "world";

18.2 导出模块

任何变量、函数、类、类型别名、接口等,都可以使用 export 来导出。

test1.ts:

const url = "http://www.baidu.com";
const person = { name: "张三", age: 18 };
    
export { url, person };

18.3 导出重命名

在导出的时候,可以使用 as 关键字进行重新命名。

test2.ts:

const url = "http://www.baidu.com";
const person = { name: "张三", age: 18 };
    
export { url as baiduUrl, person };

18.4 重新导出

重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。

test3.ts:

export * from "./test";
export * from "./test2";

这个样就可以在第三个文件中重新导出这两个模块。

18.5 默认导出

每个模块都只有一个 default 默认导出。

const url = "http://www.baidu.com";
const person = { name: "张三", age: 18 };
    
export default { url, person };
    
//export default const name = "zhangsan"; // 报错, 一个模块不能具有多个默认导出

18.6 导入模块

使用 imnport 形式来导入其它模块中的导出内容。

main.ts:

import { url, person } from "./test";
    
console.log(url);
console.log(person);

一般我们通过 import foo from 'foo' 导入一个npm包。

// 单个导入
import { foo } from 'foo';

18.7 导入重命名

导入模块时也可以使用as关键字进行重新命名。

main.ts:

import { url as baiduUrl, person } from "./test";
    
console.log(baiduUrl);
console.log(person);

18.8 import type 语句

import 在一条语句中,可以同时输入类型和正常接口。

test.ts:

const url = "http://www.baidu.com";
interface Person {
  name: string;
  age: number;
}

export { Person, url };

在另一个文件中的 import 后面加上 type 关键字。

import { type Person, url } from "./test"

console.log(url);

let person: Person = { name: "张三", age: 18 };
console.log(person);

18.9 可选的模块加载

export = 语法定义一个模块的导出对象。这里的对象指的是函数、类、接口、命名空间等。

通过 export = 语法导出的模块只能使用 import = require("module") 导入这个模块。

import = require("module") 可以让我们访问模块导出的类型,模块加载器会动态调用(通过require)。它利用了省略引用的优化,所以模块只在被需要时加载。

test.ts

const url = "http://www.baidu.com";
class Person {
  name: string;
  age: number;

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

  intro() {
    console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
  }
}

export = Person;

main.ts

import Person = require("./test")
    
let person = new Person("张三", 18)
person.intro();

19-声明文件

TypeScript 声明文件用于描述一个模块或库的类型信息,以便 TypeScript 可以检查代码的类型正确性和提供代码补全等功能。

在使用第三方库或模块时,如果没有相应的声明文件,TypeScript 无法识别它们的类型信息,就无法提供代码补全和类型检查功能。此时,我们可以手动编写声明文件来解决这个问题。

例如我们想导入 jQuery,但是在 TypeScript 中,它只认识ts文件里面的内容,根本不认识 jQuery 是什么东西。

这个时候,我们就需要使用 declare 关键字来定义它的类型,帮助 TypeScript 判断我们传入的参数类型对不对。

declare var jQuery: (selector: string) => any;
    
jQuery('#foo');

declare 定义的类型只会用于编译时的检查,编译结果中会被删除。declare 定义的类型需要写在一个专门的文件当中,这个文件就是声明文件。

19.1 声明文件

TypeScript 声明文件(Declaration Files)是一种特殊的文件,用于描述 JavaScript 模块、类、接口、命名空间、枚举、变量等的类型信息。声明文件必须使用以 .d.ts 为后缀的文件。例如:jquery.d.ts

声明文件或者模块的语法如下:

declare module moduleName {

}

全局变量的声明文件主要有以下几种语法:

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性)全局对象
  • interface 和 type 声明全局类型
// 类型 接口
interface Person { name: string; }

// 类型 类型别名
type Fruit = { size: number };

// 值 变量
declare let num: number;

// 值 函数
declare function foo(message: string): void;

// 值 类
declare class Person { name: string; }

// 值 枚举
declare enum Color { Red, Green, Blue }

// 值 命名空间
declare namespace Person { let name: string; }

如果类型没有生效,可以检查下 tsconfig.json 中的 filesincludeexclude 配置确保其包含了声明文件。

有时候我们通过 import 导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。
TypeScript 提供了一个语法 declare module,它可以用来扩展原有模块的类型。

// declare module不仅可以用于给一个第三方模块声明类型,还可以用来给第三方模块的插件模块声明类型。
// 第三方模块
declare module '模块名' {
  // declaration code......
}

如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module 扩展原有模块:

import * as moment from 'moment';
 
declare module 'moment' {
  export function foo(): moment.CalendarKey;
}

// 图片资源
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'

通过这种方式,TypeScript 可以检查代码的类型正确性,并提供代码补全等功能,从而增强代码的可维护性和减少错误。