TypeScript onePage

为什么学习 TS

  • 获得更好的开发体验
  • 解决 JS 中难以解决的问题
1function getUserName() {
2  if (Math.random() < 0.5) {
3    return "Kevin Woo";
4  }
5  return 404;
6}
7let myname = getUserName(); //名字可能错了,类型也可能错误
8myname = myname
9  .split(" ")
10  .filter((it) => it) //过滤空的
11  .map((it) => it[0].toUpperCase() + it.substr(1))
12  .join(" ");

JS 的问题

  • 使用了不存在的变量,函数或成员
  • 把一个不确定的类型当做一个确定的类型处理
  • 在使用 undefined null 的成员

JS 原罪

  • JS 语言本身特性决定了改语言无法适应大型复杂项目
  • 弱类型:某个变量,可以随时更换类型
  • 解释性:错误的发生时间,是在运行时

前端开发中,大部分时间都在排错

TypeScript

TS 是 JS 的超集,是一个可选的,静态的类型系统

类型系统:对代码中所有的标识符(变量,函数,参数,返回值)进行类型检查

静态的:无论浏览器环境,还是 node,无法直接识别 TS 代码

babel:es6--->es5 tsc:ts---->es tsc:ts 编译器

静态:类型检查发生的时间,在编译的时候,而非运行时

TS 不参与任何运行时候的类型检查,运行的是 JS 代码

TS 常识

额外的惊喜

有了类型检查,增强了面向对象的开发

JS 中, 也有类和对象,JS 支持面向对象开发。没有类型检查,很多面向对象的场景实现起来有诸多问题

使用 TS 后,可以编写出完善的面向对象代码。

在 node 中搭建 TS 开发环境

默认情况 TS 会做出假设:

  • 假设当前执行环境是 DOM
  • 如果使用代码中没有使用模块化语句,便认为改代码全局执行
  • 编译的目标代码是 ES3

有两种方式更改以上假设:

  • 使用 tsc 命令时加上选项参数
  • 使用 TS 配置文件,更改编译选项

TS 配置文件

  1. tsconfig.json
  2. tsc --init 生成配置文件

使用配置文件后,使用 tsc 进行编译时,不能跟上文件名,如果跟上的话,会忽略配置文件

直接 tsc 即可编译

1{
2  "compilerOptions": {//编译选项
3    "target":"es2016",//配置编译目标代码的版本标准
4    "module":"commonjs",//配置编译目标使用的模块化标准
5    "lib":["es2016"],//这里没有node环境可以配置,但是去掉了浏览器环境,
6    // 也不知道这是node环境,所以导致console也没有了,这里必须要安装第三大库
7    "outDir":"./dist/"//dist里面放编译结果
8  },
9  "include":["./src"]//要编译的文件夹。默认整个工程
10}

第三方库:@types/node

@types 是一个 ts 官方的类型库 其中包含了很多对 JS 代码的类型描述。(第三方库 axios,lodash,mock 等是 JS 写的,没有类型检查,我需要类型检查,就去 types 里面找有没有对应的类型库)

jQuery:js 写的,没有类型检查 安装@types/jquery,为 jquery 库添加类型定义

cnpm i -D @types/node 开发依赖,运行时候不需要,所以-D

1{
2  "compilerOptions": {
3    //...
4  },
5  "includes": ["./src"], //标识要编译的文件夹,默认所有
6  "files": ["./src/index.ts"] // 编译文件夹里面的某个文件
7}

编译和运行流程:

得先 tsc 编译 ts 在 node index.js 运行 js 嫌麻烦,所以衍生出用第三方库简化流程

第三方库简化编译运行流程

ts-node:编译完没有 dist 目录,没有 js 文件,直接运行 js

cnpm i -g ts-node 安装后,运行用 ts-node src/index.ts,编译完直接执行,没有 dist 文件

监控代码变化,变化后再次编译运行

nodemon: 用于检测文件变化

cnpm i -g nodemon

执行命令:nodemon --exec ts-node .\src\index.ts

可以把这个命令弄到 package.json 里面

1{
2  "scripts": {
3    "dev": "nodemon --exec ts-node ./src/index.ts"
4  }
5}

两个细节

  • nodemon 检测的范围太广,我只想让他检测 ts
1"dev":"nodemon -e ts --exec ts-node ./src/index.ts"//表示检测文件的拓展名ts
  • src 文件夹之外的 ts 文件
1"dev":"nodemon --watch src -e ts --exec ts-node ./src/index.ts"//只监控文件夹src的

最终可以使用 tsc 完成最后的打包

基本类型约束

TS 是一个可选的静态的类型系统

如何进行类型约束

仅需要在 变量、函数的参数、函数的返回值位置加上:类型

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

ts 在很多场景中可以完成类型推导

any: 表示任意类型,对该类型,ts 不进行类型检查

小技巧,如何区分数字字符串和数字,关键看怎么读? 如果按照数字的方式朗读,则为数字;否则,为字符串。

源代码和编译结果的差异

编译结果中没有类型约束信息

基本类型

  • number:数字
  • string:字符串
  • boolean:布尔
  • 数组
  • object: 对象
  • null 和 undefined

boolean

1function isOdd(n: number): boolean {
2  return n % 2 === 0;
3}
4console.log(isOdd(3));

数组

1let num: number[]; //num必须是数组,数组里每一项都是number
2num = [1, 2];
3// 或语法糖
4let num: Array<number> = [3, 4, 5];

object: 对象

1let u: object;
2u = {
3  name: "abc",
4  age: 19,
5  // 不能约束对象里面的东西
6};

应用场景

1// 传入一个对象,打印对象所有属性值
2function printValues(obj: object) {
3  const vals = Object.values(obj); //返回的是一个any类型的数组(不知道数组每一项是什么的数组)
4  vals.forEach((v) => {
5    console.log(v);
6  });
7}
8printValues({
9  name: "abc",
10  age: 19,
11});

null 和 undefined 是所有其他类型的子类型,它们可以赋值给其他类型

1let m: string = null; //隐患产生了
2let n: string = undefined; //隐患产生了
3n.toLocaleLowerCase(); //报错

通过在 tsconfig.json 里面添加strictNullChecks:true,可以获得更严格的空类型检查,null 和 undefined 只能赋值给自身。

其他常用类型

  • 联合类型:多种类型任选其一

配合类型保护进行判断

1let name: string | undefined; // 联合类型
2// name.的时候没有提示了
3if (typeof name === "string") {
4  // name.  确定为string出现提示了
5}
6//即类型保护

类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof 可以触发基本类型保护。

  • void 类型:通常用于约束函数的返回值,表示该函数没有任何返回

  • never 类型:通常用于约束函数的返回值,表示该函数永远不可能结束

1function test(): never {
2  throw new Error("123");
3  console.log("此处代码无法访问");
4}
1function test(): never {
2  while (true) {}
3}
  • 字面量类型:使用一个值进行约束

字符串 数组 对象

  • 元祖类型(Tuple): 一个固定长度的数组,并且数组中每一项的类型确定

  • any 类型: any 类型可以绕过类型检查,因此,any 类型的数据可以赋值给任意类型

类型别名

对已知的一些类型定义名称

type 类型名 = ...
1type Gender = "男" | "女";
2type User = {
3  name: string;
4  age: number;
5  gender: Gender;
6};
7let u: User;
8
9u = {
10  name: "ads",
11  gender: "男",
12  age: 34,
13};
14
15function getUsers(g: Gender): User[] {
16  return [];
17}
18getUsers("女");

函数的相关约束

函数重载:在函数实现之前,对函数调用的多种情况进行声明

1function combine(a: number, b: number): number;
2function combine(a: string, b: string): string;
3function combine(a: number | string, b: number | string): number | string {
4  //两个数字,相乘返回;
5  //两个字符串,拼接返回
6  if (typeof a === "number" && typeof b === "number") {
7    return a * b;
8  } else if (typeof a === "string" && typeof b === "string") {
9    return a + b;
10  }
11  throw new Error("a和b必须是相同类型");
12}
13const result1 = combine(1, 2);
14const result2 = combine("1", "2");

可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。

默认参数(一旦传递了默认参数,就默认可选)

扩展类型-枚举

扩展类型:类型别名、枚举、接口、类

枚举通常用于约束某个变量的取值范围。

字面量和联合类型配合使用,也可以达到同样的目标。

字面量类型的问题

  • 在类型约束位置,会产生重复代码。可以使用类型别名解决该问题。
1type Gender = "男" | "女";
2let gender: Gender;
3gender = "男";
4function searchGender(g: Gender) {}
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
1type Gender = "帅哥" | "美女"; //把男女改为帅哥美女
  • 字面量类型不会进入到编译结果。

枚举

如何定义一个枚举:

enum 枚举名{ 枚举字段1 = 值1, 枚举字段2 = 值2, ... }

逻辑名称和真实名称区分开来

1enum Gender{
2    male = "男",
3    female = "女"
4}
5let gender =  Gender.male;
6console.log(gender)

枚举会出现在编译结果中,编译结果中表现为对象。

1enum Gender {
2  male = "男",
3  female = "女",
4}
5let gender = Gender.male;
6
7function printGenders() {
8  const vals = Object.values(Gender);
9  vals.forEach((v) => console.log(v));
10}
11printGenders();

枚举的规则:

  • 枚举的字段值可以是字符串或数字
  • 数字枚举的值会自动自增
1enum Level {
2  level1 = 1, //不赋值就是0,后面依次递增
3  level2,
4  level3,
5}
6let l: Level = Level.level1;
7l = Level.level2;
8console.log(l); //2
  • 被数字枚举约束的变量,可以直接赋值为数字
  • 数字枚举的编译结果 和 字符串枚举有差异

最佳实践:

  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值(逻辑含义与真实区分开)

扩展:位枚举(枚举的位运算)

针对的数字枚举

位运算:两个数字换算成 2 进制后进行的计算

1enum Permission {
2  // 通过2进制位上的标识确定权限
3  Read = 1, //2^0   0001
4  Write = 2, //2^1  0010
5  Create = 4, //2^2 0100
6  Delete = 8, //2^3 1000
7}
8//3 == 0011 可读,可写
9// 1. 如何组合权限:    |:或运算
10// let p1 = Permission.Read | Permission.Write;
11let p2: Permission = Permission.Read | Permission.Write;
12
13// 2. 如何判断是否拥有某个权限
14
15function hasPermission(target: Permission, per: Permission) {
16  //target里面包不包含per
17  return (target & per) === per; //且运算
18}
19// 判断变量p2是否拥有可读权限
20hasPermission(p2, Permission.Read);
21
22// 3. 如何删除某个权限
23
24p2 = p2 ^ Permission.Write; //异或(两个位置相同取0不同取1)

模块化

相关配置:

配置名称 含义
module 设置编译结果中使用的模块化标准
moduleResolution 设置解析模块的模式
noImplicitUseStrict 编译结果中不包含"use strict"
removeComments 编译结果移除注释
noEmitOnError 错误时不生成编译结果
esModuleInterop 启用 es 模块化交互非 es 模块导出

前端领域中的模块化标准:ES6、commonjs、amd、umd、system、esnext

TS 中如何书写模块化语句 编译结果??

TS 中如何书写模块化语句

TS 中,导入和导出模块,统一使用 ES6 的模块化标准

如果不写导入,可以快速修复

必须是声明导出方式,如果使用默认导出,就没有智能导入了

1export default {
2  name: "kevin",
3  sum(a: number, b: number) {
4    return a + b;
5  },
6};

导入的时候不要添加.ts,因为编译后是 js 了,但是编译结果没有 js 文件

编译结果中的模块化

可配置:在配置文件中设置

1"module":"commonjs",

如果不希望注释也在编译的结果中:在配置文件里面写上

1{
2  "removeComments": true
3}

TS 中的模块化在编译结果中:

  • 如果编译结果的模块化标准是 ES6: 没有区别
  • 如果编译结果的模块化标准是 commonjs:导出的声明会变成 exports 的属性,默认的导出会变成 exports 的 default 属性;导入:把整个对象拿到,依次取属性

解决默认导入的错误

方案 1

方案 2 方案 3

"esModuleInterop": true

如何在 TS 中书写 commonjs 模块化代码(一般不会遇到此问题)

如果使用之前的 commonjs 的话,就失去类型检查功能了 可以这样导入(条件:必须经过 esModuleInterop 的配置):import myModule from './myModule',就有类型检查了

使用以下语法就可以解决:

导出:export = xxx

导入:import xxx = require("xxx")

模块解析

模块解析:应该从什么位置寻找模块

TS 中,有两种模块解析策略

  • classic:经典(过时)
  • node:node 解析策略(唯一的变化,是将 js 替换为 ts)
    • 相对路径require("./xxx") 先找当前路径有没有,没有的话看package.json的main.ts,没有就这个文件夹下的index.ts
    • 非相对模块require("xxx") 当前文件夹有没有node-modules,在node-modules下有没有改模块

为了防止出错,强行 node 解析策略:加上配置:"moduleResolution":"node"

接口和类型兼容性

扩展类型-接口

接口:inteface

扩展类型:类型别名、枚举、接口、类

TypeScript 的接口:用于约束类、对象、函数的契约(标准)

契约(标准)的形式:

  • API 文档,弱标准
  • 代码约束,强标准

和类型别名一样,接口,不出现在编译结果中

  1. 接口约束对象
  2. 接口约束函数

对象

1interface User {
2  name: string;
3  age: number;
4}
5// 和类型别名的区别:约束类
6/* type User = {
7    name:string,
8    age:number
9} */
10let u: User = {
11  name: "abc",
12  age: 19,
13};

函数

1interface User {
2  name: string;
3  age: number;
4  // 书写方式1    sayHello:() => void
5  // 书写方式2    sayHello():void
6  // 和类型别名一样,接口,不出现在编译结果中,所以不能写函数实现
7  sayHello(): void;
8}
9let u: User = {
10  name: "abc",
11  age: 19,
12  sayHello() {
13    console.log("asda");
14  },
15};

类型别名约束函数

1type Condition = (n: number) => boolean;
2
3function sum(numbers: number[], callBack: Condition) {
4  let s = 0;
5  numbers.forEach((n) => {
6    if (callBack(n)) {
7      s += n;
8    }
9  });
10  return s;
11}
12const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
13console.log(result);

接口约束函数

1interface Condition {
2  (n: number): boolean;
3}
4
5// 可以这样写
6/* type Condition = {//定界符
7    (n:number):boolean
8} */
9
10function sum(numbers: number[], callBack: Condition) {
11  let s = 0;
12  numbers.forEach((n) => {
13    if (callBack(n)) {
14      s += n;
15    }
16  });
17  return s;
18}
19const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
20console.log(result);

接口可以继承

1interface A {
2  T1: string;
3}
4interface B extends A {
5  //接口B里面有A里面所有成员
6  T2: number;
7}
8let u: B = {
9  T2: 33,
10  T1: "ab",
11};

可以通过接口之间的继承,实现多种接口的组合

1interface A {
2  T1: string;
3}
4interface B {
5  T2: number;
6}
7interface C extends A, B {
8  T3: boolean;
9}
10let u: C = {
11  T2: 33,
12  T1: "abc",
13  T3: true,
14};

使用类型别名可以实现类似的组合效果,需要通过&,它叫做交叉类型

1type A = {
2  T1: string;
3};
4type B = {
5  T2: number;
6};
7type C = {
8  T3: boolean;
9} & A &
10  B;
11
12let u: C = {
13  T2: 123,
14  T1: "abc",
15  T3: true,
16};

它们的区别:

  • 子接口不能覆盖父接口的成员
  • 交叉类型会把相同成员的类型进行交叉 不是覆盖 无法正常赋值了

readonly

只读修饰符,修饰的目标是只读

只读修饰符不在编译结果中

类型兼容性

B->A,如果能完成赋值,则 B 和 A 类型兼容

鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可

  • 基本类型:完全匹配
  • 对象类型:鸭子辨型法

类型断言

1interface Duck {
2  sound: "嘎嘎嘎";
3  swin(): void;
4}
5let person = {
6  name: "伪装成鸭子的人",
7  age: 11,
8  //sound: "嘎嘎嘎",这里需要类型断言,因为这里的sound:嘎嘎嘎是string,但是要求sound是嘎嘎嘎字面量
9  sound: "嘎嘎嘎" as "嘎嘎嘎",
10  swin() {
11    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
12  },
13};
14let duck: Duck = person; //这个结构满足鸭子的特征,所以可以赋值了

解决的问题:假设有个函数,用于得到服务器某个接口的返回结果,是一个用户对象。对象有很多属性,但是实际需要的属性很少,就可以用鸭子辨型法。

当直接使用对象字面量赋值的时候,会进行更加严格的判断

  • 函数类型

一切无比自然

参数:传递给目标函数的参数可以少,但不可以多

1interface Condition {
2  (n: number, i: number): boolean;
3}
4
5function sum(numbers: number[], callBack: Condition) {
6  let s = 0;
7  for (let i = 0; i < numbers.length; i++) {
8    const n = numbers[i];
9    if (callBack(n, i)) {
10      s += n;
11    }
12  }
13  return s;
14}
15// const result = sum([3, 4, 5, 6, 7], n => n % 2 !== 0);
16// const result = sum([3, 4, 5, 6, 7], (n, i) => i % 2 !== 0);
17// console.log(result);

返回值:要求返回必须返回;不要求返回,你随意;

忠告:不要死记硬背,理解 TS,舒服安全逻辑地写代码

TS 中的类

面向对象思想

基础部分,学习类的时候,仅讨论新增的语法部分。

属性

1class User {
2  constructor(name: string) {
3    this.name = name; // 报错
4  }
5  // TS不允许动态添加属性
6  // const obj={}
7  // obj.name ='aaa' 报错
8}

使用属性列表来描述类中的属性

属性的初始化检查配置

strictPropertyInitialization:true

属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

默认:性别男

方法 1:构造函数中

1class User {
2  name: string;
3  age: number;
4  gender: "男" | "女";
5  constructor(name: string, age: number, gender: "男" | "女" = "男") {
6    this.name = name;
7    this.age = age;
8    this.gender = gender;
9  }
10}
11const u = new User("abc", 19);

方法 2:属性默认值

1class User {
2  name: string;
3  age: number;
4  gender: "男" | "女" = "男";
5  constructor(name: string, age: number) {
6    this.name = name;
7    this.age = age;
8  }
9}
10const u = new User("abc", 1);
11u.gender = "女"; //可改

属性可以修饰为可选的

1pid: string;
2// ...
3pid: string | undefined;

属性可以修饰为只读的

1class User {
2  readonly id: number;
3  name: string;
4  age: number;
5  gender: "男" | "女" = "男";
6  pid?: string;
7  constructor(name: string, age: number) {
8    this.id = Math.random();
9    this.name = name;
10    this.age = age;
11  }
12}

不希望外部使用

1class User {
2  name: string;
3  age: number;
4  gender: "男" | "女" = "男";
5  publishNumber: number = 3; //每天一个可以发布多少文章
6  curNumber: number = 0; //当前可以发布的文章数
7  constructor(name: string, age: number) {
8    this.name = name;
9    this.age = age;
10  }
11}
12const u = new User("abc", 1);
13u.publishNumber; //不希望外部使用
14u.curNumber; //不希望外部使用
15// JS用Symble

使用访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:见下文

属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写。 加上的是修饰符(public private ...)

访问器

作用:用于控制属性的读取和赋值

方式 1:类似 java es6 方式 2:类似 c#

1class User {
2  constructor(public name: string, public _age: number) {}
3  set age(value: number) {
4    if (value < 0) {
5      this._age = 0;
6    } else if (value > 200) {
7      this._age = 200;
8    } else {
9      this._age = value;
10    }
11  }
12  get age() {
13    return Math.floor(this._age);
14  }
15}

泛型

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)

1function take(arr: any[], n: number): any[] {
2  if (n > arr.length) {
3    return arr;
4  }
5  const newArr: any[] = [];
6  for (let i = 0; i < n; i++) {
7    newArr.push(arr[i]);
8  }
9  return newArr;
10}
11const newArr = take(["2", 3, 4, 5, 1], 3);
12console.log(newArr);
13// any应该相同

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS 会智能的根据传递的参数,推导出泛型的具体类型

如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值(不能完成推导,又没有传递就使用默认值)

在函数中使用泛型

在函数名之后写上<泛型名称>

1function take<T>(arr: T[], n: number): T[] {
2  //T依附于函数
3  if (n > arr.length) {
4    return arr;
5  }
6  const newArr: T[] = [];
7  for (let i = 0; i < n; i++) {
8    newArr.push(arr[i]);
9  }
10  return newArr;
11}
12take<number>([1, 2], 2); //调用的时候才知道T的类型

如果不写泛型名称,会自动推导

如何在类型别名、接口、类中使用泛型

直接在名称后写上<泛型名称>

1// 回调函数:判断数组中的某一项是否满足条件
2// type callback = (n: number, i: number) => boolean;
3type callback<T> = (n: T, i: number) => boolean;
4// 封装一个filter
5function filter<T>(arr: T[], callback: callback<T>): T[] {
6  const newArr: T[] = [];
7  arr.forEach((n, i) => {
8    if (callback(n, i)) {
9      newArr.push(n);
10    }
11  });
12  return newArr;
13}
14
15const arr = [3, 4, 5, 2, 1, 6];
16console.log(filter(arr, (n) => n % 2 !== 0));
17
18// 接口
19interface callback<T> {
20  (n: T, i: number): boolean;
21}

类

1export class ArrayHelper<T> {
2  constructor(private arr: T[]) {}
3  take(n: number): T[] {
4    if (n > this.arr.length) {
5      return this.arr;
6    }
7    const newArr: T[] = [];
8    for (let i = 0; i < n; i++) {
9      newArr.push(this.arr[i]);
10    }
11    return newArr;
12  }
13  shuffle() {
14    for (let i = 0; i < this.arr.length; i++) {
15      const targetIndex = this.getRandom(0, this.arr.length);
16      const temp = this.arr[i];
17      this.arr[i] = this.arr[targetIndex];
18      this.arr[targetIndex] = temp;
19    }
20  }
21  private getRandom(min: number, max: number) {
22    const dec = max - min;
23    return Math.floor(Math.random() * dec + max);
24  }
25}
26const helper = new ArrayHelper([1, 2]); //自动类型推导
27
28helper.take(); //结果是number类型

泛型约束

泛型约束,用于现实泛型的取值

1interface hasNameProperty {
2  //nameToUpperCase函数只能传递满足此接口的类型
3  name: string;
4}
5/**
6 * 将某个对象name的属性的每个单词的首字母大写,并将该对象返回
7 */
8function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
9  obj.name = obj.name
10    .split(" ")
11    .map((s) => s[0].toUpperCase() + s.substr(1))
12    .join(" ");
13  return obj;
14}
15const o = {
16  name: "kevin",
17  age: 19,
18};
19const newO = nameToUpperCase(o);
20console.log(newO.name);

多泛型

写函数或类,依赖多种类型

1// 将两个数组混合[1,2] ['a','b'] === [1,'a',2,'b']
2function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
3  if (arr1.length !== arr2.length) {
4    throw new Error("长度不等");
5  }
6  let result: (T | K)[] = [];
7  for (let i = 0; i < arr2.length; i++) {
8    result.push(arr1[i]);
9    result.push(arr2[i]);
10  }
11  return result;
12}
13
14const result = mixinArray([1, 2, 3], ["a", "b", "c"]);
15result.forEach((it) => console.log(it));

深入理解类和接口

面向对象概述

为什么要讲面向对象

  1. TS 为前端面向对象开发带来了契机

JS 语言没有类型检查,如果使用面向对象的方式开发,会产生大量的接口,而大量的接口会导致调用复杂度剧增,这种复杂度必须通过严格的类型检查来避免错误,尽管可以使用注释或文档或记忆力,但是它们没有强约束力。

TS 带来了完整的类型系统,因此开发复杂程序时,无论接口数量有多少,都可以获得完整的类型检查,并且这种检查是据有强约束力的。

  1. 面向对象中有许多非常成熟的模式,能处理复杂问题

在过去的很多年中,在大型应用或复杂领域,面向对象已经积累了非常多的经验。

基于 ts 的 nextjs 相当于前端的 java spring

什么是面向对象

面向对象:Oriented(基于) Object(事物),简称 OO。

是一种编程思想,它提出一切以类对切入点思考问题。

其他编程思想:面向过程(模块化)、函数式编程

学开发最重要最难的是什么?思维

面向过程:以功能流程为思考切入点,不太适合大型应用

函数式编程:以数学运算为思考切入点

面向对象:以划分类为思考切入点。类是最小的功能单元

类:可以产生对象的模板。

如何学习

  1. TS 中的 OOP (面向对象编程,Oriented Object Programing)
  2. 小游戏练习

理解 -> 想法 -> 实践 -> 理解 -> ....

类的继承

继承的作用

继承可以描述类与类之间的关系

坦克、玩家坦克、敌方坦克 玩家坦克是坦克,敌方坦克是坦克

如果 A 和 B 都是类,并且可以描述为 A 是 B,则 A 和 B 形成继承关系:

  • B 是父类,A 是子类
  • B 派生 A,A 继承自 B
  • B 是 A 的基类,A 是 B 的派生类

如果 A 继承自 B,则 A 中自动拥有 B 中的所有成员

成员的重写

重写(override):子类中覆盖父类的成员

子类成员不能改变父类成员的类型

无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配。

注意 this 关键字:在继承关系中,this 的指向是动态——调用方法时,根据具体的调用者确定 this 指向

super 关键字:在子类的方法中,可以使用super 关键字读取父类成员

类型匹配

鸭子辨型法 子类的对象,始终可以赋值给父类 let p:Tank = new EnemyTank()

面向对象中,这种现象,叫做里氏替换原则

如果需要判断一个数据的具体子类类型,可以使用 instanceof

protected 修饰符

readonly:只读修饰符

访问权限修饰符:private public protected

protected: 受保护的成员,只能在自身和子类中访问

单根性和传递性

单根性:每个类最多只能拥有一个父类

传递性:如果 A 是 B 的父类,并且 B 是 C 的父类,则,可以认为 A 也是 C 的父类

抽象类

为什么需要抽象类

有时,某个类只表示一个抽象概念,主要用于提取子类共有的成员,而不能直接创建它的对象。该类可以作为抽象类。

给类前面加上abstract,表示该类是一个抽象类,不可以创建一个抽象类的对象。

抽象成员

父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或实现是什么,因此,需要有一种强约束,让继承该类的子类,必须要实现该成员。

抽象类中,可以有抽象成员,这些抽象成员必须在子类中实现**。抽象成员必须出现在抽象类中。**

1abstract class Chess {
2  x: number = 0;
3  y: number = 0;
4  abstract readonly name: string;
5}
6class Horse extends Chess {
7  // 方式1:快速修复
8  // readonly name:string = '马'
9  // 方式2
10  // readonly name: string
11  // constructor() {
12  //     super()
13  //     this.name = '跑'
14  // }
15  // 方式3:访问器
16  get name() {
17    return "兵";
18  } //没写set,本身就是只读的
19}
1abstract class Chess {
2  x: number = 0;
3  y: number = 0;
4  abstract readonly name: string;
5  abstract move(targetX: number, targetY: number): boolean;
6}
7class Horse extends Chess {
8  move(targetX: number, targetY: number): boolean {
9    this.x = targetX;
10    this.y = targetY;
11    console.log("移动成功");
12    return true;
13  }
14  get name() {
15    return "兵";
16  }
17}

设计模式 - 模板模式

设计模式:面对一些常见的功能场景,有一些固定的、经过多年实践的成熟方法,这些方法称之为设计模式。

模板模式:有些方法,所有的子类实现的流程完全一致,只是流程中的某个步骤的具体实现不一致,可以将该方法提取到父类,在父类中完成整个流程的实现,遇到实现不一致的方法时,将该方法做成抽象方法。

1abstract class Chess {
2  x: number = 0;
3  y: number = 0;
4
5  abstract readonly name: string;
6
7  move(targetX: number, targetY: number): boolean {
8    console.log("1. 边界判断");
9    console.log("2. 目标位置是否有己方棋子");
10    //3. 规则判断
11    if (this.rule(targetX, targetY)) {
12      this.x = targetX;
13      this.y = targetY;
14      console.log(`${this.name}移动成功`);
15      return true;
16    }
17    return false;
18  }
19
20  protected abstract rule(targetX: number, targetY: number): boolean;
21}
22
23class Horse extends Chess {
24  protected rule(targetX: number, targetY: number): boolean {
25    return true;
26  }
27
28  readonly name: string = "马";
29}
30
31class Pao extends Chess {
32  protected rule(targetX: number, targetY: number): boolean {
33    return false;
34  }
35
36  readonly name: string;
37
38  constructor() {
39    super();
40    this.name = "炮";
41  }
42}
43
44class Soldier extends Chess {
45  protected rule(targetX: number, targetY: number): boolean {
46    return true;
47  }
48
49  get name() {
50    return "兵";
51  }
52}
53class King extends Chess {
54  name: string = "将";
55
56  protected rule(targetX: number, targetY: number): boolean {
57    throw new Error("Method not implemented.");
58  }
59}

静态成员

什么是静态成员

静态成员是指,附着在类上的成员(属于某个构造函数的成员)

使用 static 修饰的成员,是静态成员

实例成员:对象成员,属于某个类的对象

静态成员:非实例成员,属于某个类

1class User {
2  constructor(
3    public loginId: string,
4    public loginPwd: string,
5    public name: string,
6    public age: number
7  ) {}
8  static login(loginId: string, loginPwd: string): User | undefined {
9    return undefined;
10  }
11}

静态方法中的 this

实例方法中的 this 指向的是当前对象

而静态方法中的 this 指向的是当前类

1class User {
2  static users: User[] = [];
3
4  constructor(
5    public loginId: string,
6    public loginPwd: string,
7    public name: string,
8    public age: number
9  ) {
10    //需要将新建的用户加入到数组中
11    User.users.push(this);
12  }
13
14  sayHello() {
15    console.log(
16      `大家好,我叫${this.name},今年${this.age}岁了,我的账号是${this.loginId}`
17    );
18  }
19
20  static login(loginId: string, loginPwd: string): User | undefined {
21    return this.users.find(
22      (u) => u.loginId === loginId && u.loginPwd === loginPwd
23    );
24  }
25}
26
27new User("u1", "123", "王富贵", 11);
28new User("u2", "123", "坤坤", 18);
29new User("u3", "123", "旺财", 22);
30
31const result = User.login("u3", "123");
32if (result) {
33  result.sayHello();
34} else {
35  console.log("登录失败,账号或密码不正确");
36}

设计模式 - 单例模式

单例模式:某些类的对象,在系统中最多只能有一个,为了避免开发者造成随意创建多个类对象的错误,可以使用单例模式进行强约束。

1class Board {
2  width: number = 500;
3  height: number = 700;
4
5  init() {
6    console.log("初始化棋盘");
7  }
8
9  private constructor() {} //构造函数私有,只能在内部创建对象
10
11  private static _board;
12
13  static createBoard(): Board {
14    if (this._board) {
15      return this._board;
16    }
17    this._board = new Board();
18    return this._board;
19  }
20}
21
22const b1 = Board.createBoard(); //静态方法创建
23const b2 = Board.createBoard();
24console.log(b1 === b2);

再谈接口

接口用于约束类、对象、函数,是一个类型契约。 有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗,这些动物都具有共同的特征:名字、年龄、种类名称,还包含一个共同的方法:打招呼,它们各自有各自的技能,技能是可以通过训练改变的。狮子和老虎能进行火圈表演,猴子能进行平衡表演,狗能进行智慧表演 马戏团中有以下常见的技能:

  • 火圈表演:单火圈、双火圈
  • 平衡表演:独木桥、走钢丝
  • 智慧表演:算术题、跳舞

不适用接口实现时:

  • 对能力(成员函数)没有强约束力
  • 容易将类型和能力耦合在一起

系统中缺少对能力的定义 —— 接口 面向对象领域中的接口的语义:表达了某个类是否拥有某种能力 某个类具有某种能力,其实,就是实现了某种接口 类型保护函数:通过调用该函数,会触发 TS 的类型保护,该函数必须返回 boolean 接口和类型别名的最大区别:接口可以被类实现,而类型别名不可以

接口可以继承类,表示该类的所有成员都在接口中。

索引器

对象[值],使用成员表达式

在 TS 中,默认情况下,不对索引器(成员表达式)做严格的类型检查

使用配置noImplicitAny开启对隐式 any 的检查。

隐式 any:TS 根据实际情况推导出的 any 类型

1class User {
2  [prop: string]: any; //属性名是字符串,类型是any就行
3  constructor(public name: string, public age: number) {}
4  sayHello() {}
5}
6const u = new User("aa", 22);
7u.pid = "samoia";

在索引器中,键的类型可以是字符串,也可以是数字

1class MyArray {
2  [index: number]: string;
3  0 = "asdas";
4  1 = "sdas";
5  2 = "asds";
6}
7const my = new MyArray();
8// my[0]
9// my[5] = 'a'

在类中,索引器书写的位置应该是所有成员之前

TS 中索引器的作用

  • 在严格的检查下,可以实现为类动态增加成员
  • 可以实现动态的操作类成员

在 JS 中,所有的成员名本质上,都是字符串,如果使用数字作为成员名,会自动转换为字符串。

在 TS 中,如果某个类中使用了两种类型的索引器,要求两种索引器的值类型必须匹配

1class A {
2  [prop: number]: string;
3  [prop: string]: string;
4}
5const a = new A();
6a[0] = "as";
7a["scads"] = "asa";

this 指向约束

https://yehudakatz.com/2011/08/10/understanding-javascript-function-invocation-and-this/

在 JS 中 this 指向的几种情况

明确:大部分时候,this 的指向取决于函数的调用方式

  • 如果直接调用函数(全局调用),this 指向全局对象或 undefined (启用严格模式)
  • 如果使用对象.方法调用,this 指向对象本身
  • 如果是 dom 事件的处理函数,this 指向事件处理对象

特殊情况:

  • 箭头函数,this 在函数声明时确定指向,指向函数位置的 this
  • 使用 bind、apply、call 手动绑定 this 对象

TS 中的 this

配置noImplicitThis为 true,表示不允许 this隐式的指向 any

在 TS 中,允许在书写函数时,手动声明该函数中 this 的指向,将 this 作为函数的第一个参数,该参数只用于约束 this,并不是真正的参数,也不会出现在编译结果中。

1// interface IUser {
2//     name: string,
3//     age: number,
4//     sayHello(this: IUser): void
5// }
6
7// const u: IUser = {
8//     name: "ssf",
9//     age: 33,
10//     sayHello() {
11//         console.log(this.name, this.age)
12//     }
13// }
14// const say = u.sayHello;
15
16class User {
17  constructor(public name: string, public age: number) {}
18
19  sayHello() {
20    // 类里面的this使用了严格模式,全局调用this指向undefined
21    // this指向用户对象本身
22    console.log(this, this.name, this.age);
23  }
24}

装饰器

概述

面向对象的概念(java:注解,c#:特征),decorator angular 大量使用,react 中也会用到 目前 JS 支持装饰器,目前处于建议征集的第二阶段

解决的问题

装饰器,能够带来额外的信息量,可以达到分离关注点的目的。

  • 信息书写位置的问题
  • 重复代码的问题

上述两个问题产生的根源:某些信息,在定义时,能够附加的信息量有限。

装饰器的作用:为某些属性、类、参数、方法提供元数据信息(metadata)

元数据:描述数据的数据

装饰器的本质

在 JS 中,装饰器是一个函数。(装饰器是要参与运行的)

装饰器可以修饰:

  • 类
  • 成员(属性+方法)
  • 参数
1class User {
2  // @require
3  // @range(3, 5)
4  // @description('账号')
5  loginId: string; //必须是3-5个字符
6  loginPwd: string; //必须是6-12位字符
7  age: number; //必须是数字0-100
8  gender: "男" | "女";
9}
10class Article {
11  title: string;
12}
13/**
14 * 统一的验证函数
15 * @param obj
16 */
17function validate(obj: object) {
18  for (const key in obj) {
19    const val = (obj as any)[key];
20    // 缺少该属性的验证规则
21  }
22}

类装饰器

类装饰器的本质是一个函数,该函数接收一个参数,表示类本身(构造函数本身)

使用装饰器**@得到一个函数**

在 TS 中,如何约束一个变量为类

  • Function
  • **new (参数)=>object**

在 TS 中要使用装饰器,需要开启experimentalDecorators

装饰器函数的运行时间:在类定义后直接运行

类装饰器可以具有的返回值:

  • void:仅运行函数
  • 返回一个新的类:会将新的类替换掉装饰目标
1function test(target: new () => object) {
2  return class B {
3    // 	不建议这样做
4  };
5}
6@test
7class A {}
8const a = new A();
9console.log(a); //B{}
1function test(target: new () => object) {
2  return class B extends target {
3    // 不建议这样做
4  };
5}
6@test
7class A {
8  prop1: string; //失去了类型检查
9}
10const a = new A();
11console.log(a); //B{}
1function test(target: new (...args: any[]) => object) {}
2@test
3class A {
4  prop1: string;
5  constructor(public prop2: string, public prop3: string) {} //默认不能有参数,加上剩余参数能有参数了
6}
1function test(str: string) {
2  return function (target: new (...args: any[]) => object) {};
3}
4@test("这是一个类") //要满足返回一个函数
5class A {
6  prop1: string;
7}

多个装饰器的情况:会按照后加入先调用的顺序进行调用。

1type constructor = new (...args: any[]) => object;
2function d1(target: constructor) {
3  console.log("d1");
4}
5function d2(target: constructor) {
6  console.log("d2");
7}
8@d1
9@d2
10//先输出d2 后 d1
11class A {
12  prop1: string;
13}

面试题

1type constructor = new (...args: any[]) => object;
2function d1() {
3  console.log("d1");
4  return function (target: constructor) {
5    console.log("d1 decorator");
6  };
7}
8function d2() {
9  console.log("d2");
10  return function (target: constructor) {
11    console.log("d2 decorator");
12  };
13}
14@d1() //先运行d1 d2函数,得到的d1 d2装饰器,装饰器从下到上运行
15@d2()
16class A {
17  prop1: string;
18}

成员装饰器

  • 属性

属性装饰器也是一个函数,该函数需要两个参数:

  1. 如果是静态属性,则为类本身;如果是实例属性,则为类的原型;
  2. 固定为一个字符串,表示属性名

实例属性

1type constructor = new (...args: any[]) => object;
2function d(target: any, key: string) {
3  // console.log(target === A.prototype, key)
4  if (!target.__props) {
5    target.__props = [];
6  }
7  target.__props.push(key);
8}
9class A {
10  @d
11  prop1: string;
12  @d
13  prop2: string;
14}
15// console.log((A.prototype as any).__props)
16const a = new A();
17console.log((a as any).__props);

静态属性

1function d(target: any, key: string) {
2  console.log(target, key);
3}
4class A {
5  @d
6  prop1: string;
7  @d
8  static prop2: string;
9}
  • 方法

方法装饰器也是一个函数,该函数需要三个参数:

  1. 如果是静态方法,则为类本身;如果是实例方法,则为类的原型;
  2. 固定为一个字符串,表示方法名
  3. 属性描述对象
1function d() {
2  return function (target: any, key: string, descriptor: PropertyDescriptor) {
3    // console.log(target, key, descriptor)
4    descriptor.enumerable = true; //可以更改属性描述符
5  };
6}
7class A {
8  @d()
9  method1() {}
10}

可以有多个装饰器修饰

练习:类和属性的描述装饰器

reflect-metadata 库

该库的作用:保存元数据

1import "reflect-metadata";
2@Reflect.metadata("a", "一个类")
3class A {
4  @Reflect.metadata("prop", "一个属性")
5  prop1: string;
6}
7const obj = new A();
8console.log(Reflect.getMetadata("a", A));
9console.log(Reflect.getMetadata("prop", obj, "prop1"));

class-validator 和 class-transformer 库

1import "reflect-metadata";
2import {
3  IsNotEmpty,
4  validate,
5  MinLength,
6  MaxLength,
7  Min,
8  Max,
9} from "class-validator";
10
11class RegUser {
12  @IsNotEmpty({ message: "账号不可以为空" })
13  @MinLength(5, { message: "账号必须至少有5个字符" })
14  @MaxLength(12, { message: "账号最多12个字符" })
15  loginId: string;
16
17  loginPwd: string;
18
19  @Min(0, { message: "年龄的最小值是0" })
20  @Max(100, { message: "年龄的最大值是100" })
21  age: number;
22  gender: "男" | "女";
23}
24
25const post = new RegUser();
26post.loginId = "22";
27post.age = -1;
28
29validate(post).then((errors) => {
30  console.log(errors);
31});
1import "reflect-metadata";
2import { plainToClass, Type } from "class-transformer";
3import axios from "axios";
4
5class User {
6  id: number;
7  firstName: string;
8  lastName: string;
9
10  @Type(() => Number) //告诉他age是数字
11  age: number;
12
13  getName() {
14    return this.firstName + " " + this.lastName;
15  }
16
17  isAdult() {
18    return this.age > 36 && this.age < 60;
19  }
20}
21
22axios
23  .get("https://api.myjson.com/bins/1b59tw")
24  .then((resp) => resp.data)
25  .then((users) => {
26    const us = plainToClass(User, users);
27    // console.log(us.getName(), us.isAdult())
28    for (const u of us) {
29      console.log(typeof u.age, u.age);
30    }
31  });

补充

  • 参数装饰器

依赖注入、依赖倒置

要求函数有三个参数:

  1. 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
  2. 方法名称
  3. 在参数列表中的索引
  • 关于 TS 自动注入的元数据

如果安装了reflect-metadata,并且导入了该库,并且在某个成员上添加了元数据,并且启用了emitDecoratorMetadata。

则 TS 在编译结果中,会将约束的类型,作为元数据加入到相应位置

这样一来,TS 的类型检查(约束)将有机会在运行时进行。

  • AOP(aspect oriented programming)

编程方式,属于面向对象开发。

将一些在业务中共同出现的功能块,横向切分,已达到分离关注点的目的。

1class RegUser {
2  loginId: string;
3
4  loginPwd: string;
5  // @规则
6  age: number;
7  // @规则
8  pid: string;
9
10  email: string;
11
12  /**
13   * 将用户保存到数据库
14   */
15  save() {
16    // 验证抽离出去了
17    if (validate(this)) {
18      //通过后保存数据库
19    }
20  }
21}

类型演算

根据已知的信息,计算出新的类型

三个关键字

  • typeof

TS 中的 typeof,书写的位置在类型约束的位置上。

表示:获取某个数据的类型

1const a: string = "asnd";
2let b: typeof a = "ad";
3console.log(typeof b);

当 typeof 作用于类的时候,得到的类型,是该类的构造函数

1// 当想要约束参数为构造函数的时候:
2class User {
3  loginId: string;
4  loginPwd: string;
5}
6// 方法1
7// function createUser(cls: new () => User): User {
8//     return new cls()
9// }
10// 方法2
11function createUser(cls: typeof User): User {
12  return new cls();
13}
14const u = createUser(User);
  • keyof

作用于类、接口、类型别名,用于获取其他类型中的所有成员名组成的联合类型

1interface User {
2  loginId: string;
3  loginPwd: string;
4  age: number;
5}
6// function printUserProperty(obj: User, prop: string) {
7//     console.log(obj[prop])//报错,因为不确定prop是loginId,loginPwd,age之一
8// }
9function printUserProperty(obj: User, prop: "loginId" | "loginPwd" | "age") {
10  console.log(obj[prop]);
11}
12const u: User = {
13  loginId: "sada",
14  loginPwd: "sss",
15  age: 22,
16};
17printUserProperty(u, "age");
18
19// 场景:某个类型应该是某个类型所有字段中的一个

应用 keyof

1interface User {
2  loginId: string;
3  loginPwd: string;
4  age: number;
5}
6function printUserProperty(obj: User, prop: keyof User) {
7  console.log(obj[prop]);
8}
9const u: User = {
10  loginId: "sada",
11  loginPwd: "sss",
12  age: 22,
13};
14printUserProperty(u, "age");
  • in

该关键字往往和 keyof 联用,限制某个索引类型的取值范围。

1interface User {
2  loginId: string;
3  loginPwd: string;
4  age: number;
5}
6type Obj = {
7  // [p:string]:string//太宽泛,什么属性都可以加
8
9  [p in "loginId" | "loginPwd" | "age"]: string; //  []叫索引器
10  /**
11   * 等价于
12   * loginId:string
13   * loginPwd:string
14   * age:string
15   */
16};
17const u: Obj = {
18  age: "1",
19  loginId: "a",
20  loginPwd: "aa",
21};
22// u.abc = '123'
23// 只能加"loginId" | "loginPwd" | "age"中的一个
24u.age = "1";

用 keyof 简化

1interface User {
2  loginId: string;
3  loginPwd: string;
4  age: number;
5}
6// 类型演算:将User所有类型变成字符串,得到一个新类型
7type Obj = {
8  [p in keyof User]: string;
9};
10const u: Obj = {
11  age: "1",
12  loginId: "a",
13  loginPwd: "aa",
14};
15u.age = "1";
1interface User {
2  loginId: string;
3  loginPwd: string;
4  age: number;
5}
6type Obj = {
7  [p in keyof User]: User[p]; //不改变User里面的的类型,直接取出来附上
8};
9const u: Obj = {
10  age: 1,
11  loginId: "a",
12  loginPwd: "aa",
13};
14u.age = 1;
1type Obj = {
2  readonly [p in keyof User]: User[p]; //类型不变,全部变成只读
3};

配合泛型:强大了

1import { type } from "os";
2
3interface User {
4  loginId: string;
5  loginpwd: string;
6}
7
8interface Article {
9  title: string;
10  publishDate: Date;
11}
12//将User的所有属性值类型变成字符串,得到一个新类型
13type String<T> = {
14  [p in keyof T]: string;
15};
16
17type Readonly<T> = {
18  readonly [p in keyof T]: T[p];
19};
20
21type Partial<T> = {
22  [p in keyof T]?: T[p];
23};
24
25const u: String<Article> = {
26  title: "Sfsdf",
27  publishDate: "sdf",
28};

TS 中预设的类型演算

1Partial<T>; // 将类型T中的成员变为可选
2
3Required<T>; // 将类型T中的成员变为必填
4
5Readonly<T>; // 将类型T中的成员变为只读
6
7Exclude<T, U>; // 从T中剔除可以赋值给U的类型。
8
9Extract<T, U>; // 提取T中可以赋值给U的类型。
10
11NonNullable<T>; // 从T中剔除null和undefined。
12
13ReturnType<T>; // 获取函数返回值类型。
14
15InstanceType<T>; // 获取构造函数类型的实例类型。

用法:

1interface User {
2  age: number;
3  name: string;
4}
5let u: Partial<User>;
6u = {
7  age: 23,
8};
1// let u: Exclude<"a" | "b" | "c" | "d", "b" | "c">
2
3type T = "男" | "女" | null | undefined;
4type NEWT = Exclude<T, null | undefined>;
5
6type func = () => number;
7type returnType = ReturnType<func>; //传入的是函数类型
8
9function sum(a: number, b: number) {
10  return a + b;
11}
12let a: ReturnType<typeof sum>; //传入的是函数类型

声明文件

概述、编写、发布

概述

  1. 什么是声明文件?

以.d.ts结尾的文件

  1. 声明文件有什么作用?

为 JS 代码提供类型声明

  1. 声明文件的位置
  • 放置到 tsconfig.json 配置中包含的目录中
  • 放置到 node_modules/@types 文件夹中
  • 手动配置
  • 与 JS 代码所在目录相同,并且文件名也相同的文件。用 ts 代码书写的工程发布之后的格式。

编写声明文件

手动编写   自动生成

  • 自动生成

工程是使用 ts 开发的,发布(编译)之后,是 js 文件,发布的是 js 文件。

如果发布的文件,需要其他开发者使用,可以使用声明文件,来描述发布结果中的类型。

配置tsconfig.json中的declaration:true即可

  • 手动编写
  1. 对已有库,它是使用 js 书写而成,并且更改该库的代码为 ts 成本较高,可以手动编写声明文件
  2. 对一些第三方库,它们使用 js 书写而成,并且这些第三方库没有提供声明文件,可以手动编写声明文件。

全局声明

声明一些全局的对象、属性、变量

namespace: 表示命名空间,可以将其认为是一个对象,命名空间中的内容,必须通过命名空间.成员名访问

模块声明

三斜线指令

在一个声明文件中,包含另一个声明文件

发布

  1. 当前工程使用 ts 开发

编译完成后,将编译结果所在文件夹直接发布到 npm 上即可

  1. 为其他第三方库开发的声明文件

发布到@types/**中。

1) 进入 github 的开源项目:https://github.com/DefinitelyTyped/DefinitelyTyped

2) fork 到自己的开源库中

3) 从自己的开源库中克隆到本地

4) 本地新建分支(例如:mylodash4.3),在新分支中进行声明文件的开发

在types目录中新建文件夹,在新的文件夹中开发声明文件

5) push 分支到你的开源库

6) 到官方的开源库中,提交 pull request

7) 等待官方管理员审核(1 天)

审核通过之后,会将你的分支代码合并到主分支,然后发布到 npm。

之后,就可以通过命令npm install @types/你发布的库名