TypeScript onePage
为什么学习 TS
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 配置文件
- tsconfig.json
- 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
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 可以触发基本类型保护。
1function test(): never {
2 throw new Error("123");
3 console.log("此处代码无法访问");
4}
1function test(): never {
2 while (true) {}
3}
字符串
数组
对象
类型别名
对已知的一些类型定义名称
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{
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 的接口:用于约束类、对象、函数的契约(标准)
契约(标准)的形式:
和类型别名一样,接口,不出现在编译结果中
- 接口约束对象
- 接口约束函数
对象
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:构造函数中
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));
深入理解类和接口
面向对象概述
为什么要讲面向对象
- TS 为前端面向对象开发带来了契机
JS 语言没有类型检查,如果使用面向对象的方式开发,会产生大量的接口,而大量的接口会导致调用复杂度剧增,这种复杂度必须通过严格的类型检查来避免错误,尽管可以使用注释或文档或记忆力,但是它们没有强约束力。
TS 带来了完整的类型系统,因此开发复杂程序时,无论接口数量有多少,都可以获得完整的类型检查,并且这种检查是据有强约束力的。
- 面向对象中有许多非常成熟的模式,能处理复杂问题
在过去的很多年中,在大型应用或复杂领域,面向对象已经积累了非常多的经验。
基于 ts 的 nextjs 相当于前端的 java spring
什么是面向对象
面向对象:Oriented(基于) Object(事物),简称 OO。
是一种编程思想,它提出一切以类对切入点思考问题。
其他编程思想:面向过程(模块化)、函数式编程
学开发最重要最难的是什么?思维
面向过程:以功能流程为思考切入点,不太适合大型应用
函数式编程:以数学运算为思考切入点
面向对象:以划分类为思考切入点。类是最小的功能单元
类:可以产生对象的模板。
如何学习
- TS 中的 OOP (面向对象编程,Oriented Object Programing)
- 小游戏练习
理解 -> 想法 -> 实践 -> 理解 -> ....
类的继承
继承的作用
继承可以描述类与类之间的关系
坦克、玩家坦克、敌方坦克
玩家坦克是坦克,敌方坦克是坦克
如果 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}
成员装饰器
属性装饰器也是一个函数,该函数需要两个参数:
- 如果是静态属性,则为类本身;如果是实例属性,则为类的原型;
- 固定为一个字符串,表示属性名
实例属性
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}
方法装饰器也是一个函数,该函数需要三个参数:
- 如果是静态方法,则为类本身;如果是实例方法,则为类的原型;
- 固定为一个字符串,表示方法名
- 属性描述对象
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 });
补充
依赖注入、依赖倒置
要求函数有三个参数:
- 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
- 方法名称
- 在参数列表中的索引
如果安装了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}
类型演算
根据已知的信息,计算出新的类型
三个关键字
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);
作用于类、接口、类型别名,用于获取其他类型中的所有成员名组成的联合类型
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");
该关键字往往和 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>; //传入的是函数类型
声明文件
概述、编写、发布
概述
- 什么是声明文件?
以.d.ts结尾的文件
- 声明文件有什么作用?
为 JS 代码提供类型声明
- 声明文件的位置
- 放置到 tsconfig.json 配置中包含的目录中
- 放置到 node_modules/@types 文件夹中
- 手动配置
- 与 JS 代码所在目录相同,并且文件名也相同的文件。用 ts 代码书写的工程发布之后的格式。
编写声明文件
手动编写 自动生成
工程是使用 ts 开发的,发布(编译)之后,是 js 文件,发布的是 js 文件。
如果发布的文件,需要其他开发者使用,可以使用声明文件,来描述发布结果中的类型。
配置tsconfig.json中的declaration:true即可
- 对已有库,它是使用 js 书写而成,并且更改该库的代码为 ts 成本较高,可以手动编写声明文件
- 对一些第三方库,它们使用 js 书写而成,并且这些第三方库没有提供声明文件,可以手动编写声明文件。
全局声明
声明一些全局的对象、属性、变量
namespace: 表示命名空间,可以将其认为是一个对象,命名空间中的内容,必须通过命名空间.成员名访问
模块声明
三斜线指令
在一个声明文件中,包含另一个声明文件
发布
- 当前工程使用 ts 开发
编译完成后,将编译结果所在文件夹直接发布到 npm 上即可
- 为其他第三方库开发的声明文件
发布到@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/你发布的库名