最新TypeScript超详细笔记(全)

文章目录

  • 一、初体验
    • 1.0 学习目标
    • 1.1 官网
    • 1.2 环境搭建
      • 安装Node.js
      • 安装TypeScript编译器
    • 1.3 编写代码
    • 1.4 编译执行
      • --outDir
      • --target
      • --watch
    • 1.5 编译配置文件
      • 扩展库
      • 指定加载配置文件
    • 1.6 ts-node
      • 安装
      • 使用
  • 二、类型系统介绍
    • 2.0 学习目标
    • 2.1 什么是类型
      • 数据是有格式的
      • 程序是可能有错误的
      • 动态类型语言&静态类型语言
      • 静态类型语言的优缺点
      • 动态类型语言
    • 2.2 什么是类型系统
      • 类型标注
      • 类型检测
    • 2.3 类型标注
    • 2.4 基础的简单的类型标注
      • 基础类型
      • 空和未定义类型
        • 小技巧
      • 对象类型
        • 内置对象类型
        • 自定义对象类型
          • 字面量标注
          • 接口
          • 类与构造函数
          • 扩展
      • 数组类型
        • 使用泛型标记
        • 简单标注
      • 元组类型
      • 枚举类型
        • 数字类型枚举
        • 字符串类型枚举
      • 无值类型
      • Never类型
      • 任意类型
      • 未知类型
      • 函数类型
  • 三、接口
    • 3.0 学习目标
    • 3.1 接口定义
    • 3.2 可选属性
    • 3.3 只读属性
    • 3.4 任意属性
      • 数字类型索引
      • 字符串类型索引
    • 3.5 使用接口描述函数
    • 3.6 接口合并
  • 四、高级类型
    • 4.0 学习目标
    • 4.1 联合类型
    • 4.2 交叉类型
    • 4.3 字面量类型
    • 4.4 类型别名
      • 使用类型别名定义函数类型
      • interface与type的区别
        • interface
        • type
    • 4.5 类型推导
    • 4.6 类型断言
  • 五、函数详解
    • 5.0 学习目标
    • 5.1函数的标注
    • 5.2 可选参数和默认参数
      • 可选参数
      • 默认参数
      • 剩余参数
    • 5.3 函数中的this
      • 普通函数
      • 箭头函数
    • 5.4 函数重载
  • 六、面向对象编程
    • 6.0 学习目标
    • 6.1 类
      • 类的基础
      • class
      • 构造函数
      • 成员属性与方法定义
      • this关键字
      • 构造函数参数属性
    • 6.2 继承
      • super关键字
        • 方法的重写与重载
    • 6.3 修饰符
      • public修饰符
      • protected修饰符
      • private修饰符
      • readonly修饰符
    • 6.4 寄存器
      • getter
      • setter-组件
    • 6.5 静态成员
    • 6.6 抽象类
      • abstract关键字
    • 6.7 类与接口
      • implements
    • 6.8 类与对象类型
    • 构造函数的类型( `typeof Person` )
  • 七、类型系统深入
    • 7.0 学习目标
    • 7.1 类型保护
      • typeof
      • instanceof
      • in
      • 字面量类型保护
      • 自定义类型保护
    • 7.2 类型操作
      • typeof
      • keyof
      • in
    • 7.3 类型兼容
  • 八、泛型
    • 8.0 学习目标
    • 8.1 为什么要使用泛型
    • 8.2 泛型的使用-函数
    • 8.3 泛型类
    • 8.4 泛型接口
  • 九、模块系统
    • 9.0 学习目标
    • 9.1 模块化
    • 9.2 模块化历程
    • 9.3 基于服务端、桌面端的模块化
      • CommonJS
    • 9.4 基于浏览器的模块化
      • AMD
        • requireJS
      • ~~`requireJS` 的 `CommonJS` 风格~~
      • CMD
      • UMD
    • 9.5 模块化的大同世界
      • ESM
        • 独立模块作用域
        • 导出模块内部数据
        • 导入外部模块数据
    • 9.6 TypeScript 中的模块化
      • TS 模块系统
        • 模块
        • 全局模块
        • 文件模块
      • 模块语法
        • 导出模块内部数据
        • 导入外部模块数据
      • 模块编译
        • `module` 选项
      • 模块导出默认值的问题
      • 加载非 `TS` 文件
        • 非 `ESM` 模块中的默认值问题
        • 以模块的方式加载 JSON 格式的文件
    • 9.7 命名空间
    • 9.8 模块解析策略
      • 什么是模块解析
      • 相对与非相对模块导入
        • 相对导入
        • 非相对导入
      • 模块解析策略
        • --moduleResolution 选项
      • Classic 模块解析策略
        • 相对导入
        • 非相对导入
      • Node 模块解析策略
        • 相对导入
        • 非相对导入
      • TypeScript 模块解析策略
  • 十、 装饰器
    • 10.0 学习目标
    • 10.1 什么是装饰器
    • 10.2 装饰器语法
    • 10.3 装饰器
      • 类装饰器
      • 方法装饰器
      • 属性装饰器
      • 访问器装饰器
      • 参数装饰器
    • 10.4 装饰器执行顺序
    • 10.5 装饰器工厂
    • 10.6 元数据
      • 什么是元数据?
    • 10.7 使用 `reflect-metadata`
      • 定义元数据
      • 使用元数据的 log 装饰器
    • 10.8 使用 `emitDecoratorMetadata`

一、初体验

1.0 学习目标

  • 学会搭建 TypeScript 环境
  • 掌握 TypeScript 代码的编译与运行

1.1 官网

https://www.typescriptlang.org/zh/

1.2 环境搭建

  • TypeScript 编写的程序并不能直接通过浏览器运行,需要先通过 TypeScript 编译器把TypeScript 代码编译成 JavaScript 代码
  • TypeScript的编译器是基于Node.js的,所以需要先安装Node.js

安装Node.js

  • https://nodejs.org
  • 安装完成以后,可以通过 终端 或者 cmd 等命令行工具来调用 node
# 查看当前 node 版本
node -v

安装TypeScript编译器

通过 NPM 包管理工具安装 TypeScript 编译器

npm i -g typescript

安装完成以后,我们可以通过命令 tsc 来调用编译器

# 查看当前 tsc 编译器版本
tsc -v

1.3 编写代码

  • vsCodeTypeScript 都是微软的产品, vsCode 本身就是基于 TypeScript 进行开发的, vsCodeTypeScript 有着天然友好的支持
  • 默认情况下, TypeScript 的文件的后缀为 .ts
// ./src/helloKaiKeBa.ts
let str: string = '开课吧';

1.4 编译执行

  • 使用安装的 TypeScript 编译器 tsc.ts 文件进行编译
  • 默认情况下会在当前文件所在目录下生成同名的 js 文件
  • 还有几个常用的编译选项:outDir、target、watch,见下方
#此时文件是在src的上一级
tsc ./src/helloKaiKeBa.ts

–outDir

指定编译文件输出目录

tsc --outDir ./dist ./src/helloKaiKeBa.ts

–target

指定编译的代码版本目标,默认为 ES3

  • ES3中,let会被转成var
  • ES6中,let会被转成let
  • ES3中,对象最后一个属性后面的逗号是不允许存在的,ES5以后就可以
tsc --outDir ./dist --target ES6 ./src/helloKaiKeBa.ts

–watch

在监听模式下运行,当文件发生改变的时候自动编译

tsc --outDir ./dist --target ES6 --watch ./src/helloKaiKeBa.ts

1.5 编译配置文件

  • 如果每次编译都输入这么一大堆的选项其实是很繁琐的
  • 好在 TypeScript 编译为提供了一个更加强大且方便的方式,编译配置文件tsConfig.json
  • 可以把编译的一些选项保存在一个指定的 json 文件中,默认情况下 tsc 命令运行的时候会自动去加载运行命令所在的目录下的 tsconfig.json 文件,配置文件格式如下
{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es5",
        "watch": true,
        // 检测null和undefined的标注
        "strictNullChecks": true,
        // 函数参数出现隐含的any时会报错
        "noImplicitAny": true,
        "lib": ["ES6", "DOM"],
        // 导入导出用commonjs还是ESM,见9.6 模块编译
        "module": "es6",
        // 允许加载JS文件
        "allowJs": true,
        // 导入导出策略
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        //允许加载json
        "resolveJsonModule": true,
        // 解析策略
        "moduleResolution": "node",
        // 装饰器是否可用
        "experimentalDecorators": true,
        // 开启后会给类、方法、访问符、属性、参数等添加几个元数据
        "emitDecoratorMetadata": true
    },
    // ** : 所有目录(包括子目录)
 	// * : 所有文件,也可以指定类型 *.ts
    "include": ["./src/**/*"]
}
  • 有了单独的配置文件以后,可以直接运行
tsc

扩展库

  • TypeScript 在编译过程中只会转换语法,比如扩展运算符,箭头函数等
  • 对于API 是不会进行转换的(也没必要转换,而是引入一些扩展库进行处理的)
  • 如果代码中使用了 target 中没有的 API ,则需要手动进行引入
  • 默认情况下 TypeScript 会根据target 载入核心的类型库
    • targetes5 时: ["dom", "es5", "scripthost"]
    • targetes6 时: ["dom", "es6", "dom.iterable", "scripthost"]
    • 如果代码中使用了这些默认载入库以外的代码,则可以通过 lib 选项来进行设置

指定加载配置文件

  • 使用 --project-p 指定配置文件目录,会默认加载该目录下的 tsconfig.json 文件
  • 此时tsconfig.json这个文件是存放在configs文件夹下的
# configs文件夹下只有一个文件
tsc -p ./configs

也可以指定某个具体的配置文件

tsc -p ./configs/ts.json

1.6 ts-node

可以直接运行ts文件,不需要经过编译

安装

npm i -g ts-node

使用

ts-node ./src/1-装饰器.ts

二、类型系统介绍

2.0 学习目标

  • 了解类型系统
    • 类型标注
    • 类型检测的好处
    • 使用场景
  • 掌握常用的类型标注的使用

2.1 什么是类型

程序 = 数据结构 + 算法 = 各种格式的数据 + 处理数据的逻辑

数据是有格式的

  • 数字、布尔值、字符
  • 数组、集合

程序是可能有错误的

  • 计算错误(对非数字类型数据进行一些数学运算)
  • 调用一个不存在的方法
  • 不同类型的数据有不同的操作方式或方法,如:字符串类型的数据就不应该直接参与数学运算

动态类型语言&静态类型语言

  • 动态类型语言
    • 程序运行期间才做数据类型检查的语言,如:JavaScript
  • 静态类型语言
    • 程序编译期间做数据类型检查的语言,如:Java

静态类型语言的优缺点

  • 优点
    • 程序编译阶段(配合IDE、编辑器甚至可以在编码阶段)即可发现一些潜在错误,避免程序在生产环境运行了以后再出现错误
    • 编码规范、有利于团队开发协作、也更有利于大型项目开发、项目重构
    • 配合IDE、编辑器提供更强大的代码智能提示/检查
    • 代码即文档
  • 缺点
    • 麻烦
    • 缺少灵活性

动态类型语言

  • 优点
    • 静态类型语言的缺点
  • 缺点
    • 静态类型语言的优点

2.2 什么是类型系统

类型系统包含两个重要组成部分

  • 类型标注(定义、注解) - typing
  • 类型检测(检查) - type-checking

类型标注

  • 类型标注就是在代码中给数据(变量、函数(参数、返回值))添加类型说明
  • 当一个变量或者函数(参数)等被标注以后就不能存储或传入与标注类型不符合的类型
  • 有了标注, TypeScript 编译器就能按照标注对这些数据进行类型合法检测
  • 有了标注,各种编辑器、IDE等就能进行智能提示

类型检测

  • 顾名思义,就是对数据的类型进行检测。注意这里,重点是类型两字。
  • 类型系统检测的是类型,不是具体值(虽然,某些时候也可以检测值)
    • 比如某个参数的取值范围(1-100之间)
    • 我们不能依靠类型系统来完成这个检测,它应该是我们的业务层具体逻辑
    • 类型系统检测的是它的值类型是否为数字

2.3 类型标注

  • TypeScript 中,类型标注的基本语法格式为
    • 数据载体:类型
  • TypeScript 的类型标注,我们可以分为
    • 基础的简单的类型标注
    • 高级的深入的类型标注

2.4 基础的简单的类型标注

  • 基础类型标注
    • 字符串、数字、布尔值、空、未定义
  • 非基础类型标注
    • 对象、数组
  • 特殊类型
    • 元组、枚举、无值类型、Never类型、任意类型、未知类型
  • 函数基本标注

基础类型

基础类型包含:stringnumberboolean

标注语法

let title: string = '开课吧';
let n: number = 100;
let isOk: boolean = true;

空和未定义类型

因为在 NullUndefined 这两种类型有且只有一个值,在标注一个变量为 NullUndefined 类型,那就表示该变量不能修改了

let a: null;
// ok
a = null;
// error
a = 1;

默认情况下 nullundefined 是所有类型的子类型。 就是说可以把 nullundefined 赋值给其它类型的变量

也就是说声明了其他类型的时候可以赋值为null或者undefined`

let a: number;
// ok
a = null;

如果一个变量声明了,但是未赋值,那么该变量的值为 undefined ,但是如果它同时也没有标注类型的话,默认类型为 anyany 类型后面有详细说明

// 类型为 `number`,值为 `undefined`
let a: number;
// 类型为 `any`,值为 `undefined`

小技巧

因为 nullundefined 都是其它类型的子类型,所以默认情况下会有一些隐藏的问题

let a:number;
a = null;
// ok(实际运行是有问题的)
a.toFixed(1);

小技巧:指定 strictNullChecks 配置为 true ,可以有效的检测 null 或者 undefined ,避免很多常见问题,也可以使程序编写更加严谨

  • tsconfig.json中添加配置
  • 严格定义检查
  • 设置以后,就不可以将null类型赋值给其他类型
let a:number;
a = null;
// error
a.toFixed(1);

let ele = document.querySelector('div');
// 获取元素的方法返回的类型可能会包含 null,所以最好是先进行必要的判断,再进行操作
if (ele) {
ele.style.display = 'none';
}

对象类型

内置对象类型

JavaScript 中,有许多的内置对象,比如:ObjectArrayDateRegExp……,我们可以通过对象的 构造函数 或者 类 来进行标注

注意,这个字母开头是大写

let a: object = {};
// 数组这里标注格式有点不太一样,后面我们在数组标注中进行详细讲解
let arr: Array<number> = [1,2,3];
let d1: Date = new Date();

自定义对象类型

另外一种情况,许多时候,我们可能需要自定义结构的对象。这个时候,我们可以:

  • 字面量标注
  • 接口
  • 定义 类 或者 构造函数
字面量标注
  • 优点:方便、直接
  • 缺点:不利于复用和维护
let a: {username: string; age: number} = {
 username: 'zMouse',
 age: 35
};
// ok
a.username;
a.age;
// error
a.gender;

接口
  • 优点:复用性高
  • 缺点:接口只能作为类型标注使用,不能作为具体值,它只是一种抽象的结构定义,并不是实体,没有具体功能实现
  • interface在编译后里面是没有的
// 这里使用了 interface 关键字,在后面的接口章节中会详细讲解
// 里面用的分号
interface Person {
 username: string;
 age: number;
};
let a: Person = {
 username: 'zMouse',
 age: 35
};
// ok
a.username;
a.age;
// error
a.gender;

类与构造函数
  • 优点:功能相对强大,定义实体的同时也定义了对应的类型
  • 缺点:复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单
  • 定义的解构中包含具体功能用class,只是定义个解构用interface
// 类的具体使用,也会在后面的章节中讲解
class Person {
constructor(public username: string, public age: number) {
}
}
// ok
a.username;
a.age;
// error
a.gender;

// 函数
interface AjaxOptions {
  url: string;
  method: string;
}
function ajax(options: AjaxOptions) {}
ajax({
  url: '',
  method: 'get'
});

扩展
  • 包装对象:其实就是 JavaScript 中的 StringNumberBoolean
  • string 类型 和 String 类型并不一样,在 TypeScript 中也是一样
    • string是字符串
    • new String是字符串对象
    • 字符串对象有的,字符串不一定有
    • 字符串对象赋值给字符串的时候会损失数据
    • 而字符串赋值给字符串对象的时候则不会
let a: string;
a = '1';
// error String有的,string不一定有(对象有的,基础类型不一定有)
a = new String('1');
let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';

数组类型

TypeScript中的数组是:一类具有相同特性的数据的有序结合

  • 相同类型的数据
  • 有序

TypeScript 中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型

使用泛型标记
//  表示数组中存储的数据类型,泛型具体概念后续会讲
let arr1: Array<number> = [];
// ok
arr1.push(100);
// error
arr1.push('开课吧');

简单标注
let arr2: string[] = [];
// ok
arr2.push('开课吧');
// error
arr2.push(1);

元组类型

元组类似数组,但是存储的元素类型不必相同,但是需要注意:

  • 初始化数据的个数以及对应位置标注类型必须一致
  • 越界数据(也就是新添加的数据)必须是元组标注中的类型之一(标注越界数据可以不用对应顺序 - 联合类型)
let data1: [string, number] = ['开课吧', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);

枚举类型

枚举的作用组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字

注意事项:

  • key 不能是数字
  • value
    • 可以是数字,称为 数字类型枚举
    • 也可以是字符串,称为 字符串类型枚举
    • 但不能是其它值,默认为数字:0
数字类型枚举
  • 枚举值可以省略,如果省略,则:
    • 第一个枚举值默认为:0
    • 非第一个枚举值为上一个数字枚举值 + 1
  • 枚举值为只读(常量)
    • 所以变量名为全大写的
    • 初始化后不可修改
  • 看下方的自枚举
enum HTTP_CODE {
 OK = 200,
 NOT_FOUND = 404,
 METHOD_NOT_ALLOWED
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;

// 上述编译后的结果
// HTTP_CODE['OK']=200
// HTTP_CODE[200]='OK'
// 自枚举
var HTTP_CODE;
(function (HTTP_CODE) {
    HTTP_CODE[HTTP_CODE["OK"] = 200] = "OK";
    HTTP_CODE[HTTP_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
    HTTP_CODE[HTTP_CODE["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
})(HTTP_CODE || (HTTP_CODE = {}));

字符串类型枚举
  • 枚举类型的值,也可以是字符串类型
  • 注意:如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
  • 枚举名称可以是大写,也可以是小写,推荐使用全大写(通常使用全大写的命名方式来标注值为常量)
enum URLS {
 USER_REGISETER = '/user/register',
 USER_LOGIN = '/user/login',
 // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
 INDEX = 0
}

无值类型

  • 表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为: void
  • strictNullChecksfalse 的情况下, undefinednull 都可以赋值给 void
  • strictNullCheckstrue 的情况下,只有 undefined 才可以赋值给 void
function fn():void {
// 没有 return 或者 return undefined
}

Never类型

  • 当一个函数永远不可能执行 return 的时候,返回的就是 never
  • void 不同, void 是执行了return , 只是没有值
  • never 是不会执行 return ,比如抛出错误,导致函数终止执行
function fn(): never {
throw new Error('error');
}

任意类型

  • 有的时候,我们并不确定这个值到底是什么类型或者不需要对该值进行类型检测,就可以标注为 any类型
  • 一个变量申明未赋值且未标注类型的情况下,默认为 any 类型
  • 任何类型值都可以赋值给 any 类型
  • any 类型也可以赋值给任意类型
  • any 类型有任意属性和方法
  • 注意:标注为 any 类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示
  • 小技巧:当指定 noImplicitAny 配置为 true ,当函数参数出现隐含的 any 类型时报错
// 隐式的any
let a;
let a1:any;
a1='a'
a1='2'

let b:number;
b=a;

未知类型

unknow,3.0 版本中新增,属于安全版的 any,但是与 any 不同的是:

  • unknow 仅能赋值给 unknowany
  • unknow 没有任何属性和方法
let c:unknown='开课吧';
let b:number=1;
b.toFixed(1);
// 下面会报错
b=c;

函数类型

JavaScript 函数是非常重要的,在 TypeScript 也是如此。同样的,函数也有自己的类型标注格式

  • 函数名称( 参数1: 类型, 参数2: 类型… ): 返回值类型;
  • 函数更多的细节内容,在后期有专门的章节来进行深入的探讨
function add(x: number, y: number): number {
return x + y;
}

三、接口

3.0 学习目标

  • 理解接口的概念
  • 学会通过接口标注复杂结构的对象

3.1 接口定义

  • TypeScript的核心之一就是对值(数据)所具有的结构就行类型检查
  • 接口(interface):对复杂的对象类型进行标注的一种方式,或者给其它代码定义一种契约(比如:类)
  • 接口中多个属性之间可以使用 逗号 或者 分号 进行分隔
  • 注意:接口是一种 类型 ,不能作为 值 使用,见下面错误的代码
// 定义接口
// 多个属性之间可以用逗号或者分号进行分隔
interface Point {
  x: number;
  y: number;
}
let p1: Point = {
  x: 100,
  y: 100
};

let p2 = Point; //错误

3.2 可选属性

  • 接口也可以定义可选的属性,通过 ? 来进行标注
  • 通过?标注的属性有两个类型可选:自身定义的,以及undefined
// color? 表示该属性是可选的
interface Point {
  x: number;
  y: number;
  color?: string;
}

3.3 只读属性

  • 可以通过 readonly 来标注属性为只读
  • 标注了一个属性为只读,那么该属性除了初始化以外,是不能被再次赋值的
interface Point {
  x: number;
  readonly y: number;
}
let p1: Point = {
  x: 100,
  y: 100
};
p1.y=200	//报错

3.4 任意属性

  • 希望给一个接口添加任意属性,可以通过索引类型来实现
  • [prop: string] : number
    • 中括号内,定义索引签名参数的类型
      • prop就是一个标识,可以随便起名
    • 后面是值的类型
  • 这个任意属性就是开个接口,可以自定义属性名和值的范围

数字类型索引

interface Point {
  x: number;
  y: number;
  color?: string;
  // 下面的索引类型也满足了上面color
  // 会报错:类型string|undefined属性的color,不能赋值给字符串索引类型number
 [prop: string]: number;
}

// 解决办法1
color?:string;
[prop:number]:number

// 解决办法2
color? :number;
[prop:string]:number | undefined;

字符串类型索引

  • 数字索引是字符串索引的子类型
interface Point {
  x: number;
  y: number;
 [prop: string]: number;
}

  • 注意:
    • 索引签名参数类型必须为 string 或 number 之一,但两者可同时出现
    • 当同时存在数字类型索引和字符串类型索引的时候,数字类型的值 其类型必须是字符串类型的值类型或子类型
interface Point {
 [prop1: string]: string;
 [prop2: number]: string;
}

interface Point1 {
 [prop1: string]: string;
 [prop2: number]: number; // 错误
}
interface Point2 {
 [prop1: string]: Object;
 [prop2: number]: Date; // 正确
}

class Person {
  constructor(public username: string) {}
}
class Student extends Person {}
interface Point {
  [key: string]: Person;
  [key: number]: Student;
}

3.5 使用接口描述函数

  • 使用接口来描述一个函数
  • 使用接口来单独描述一个函数,是没 key 的
  • 作用
    • 为了复用
    • 回调函数
interface IFunc {
	// fn(a:string):string;		// 这种写法是错误的,他定义出来的是IFunc的一个方法
	(a: string): string;
}
let fn: IFunc = function(a) {}
let fn1: IFunc = function(a){}

interface IFunc{
  (x:number,y:number):number
}
let fn1: IFunc = function (a, b) {
  return a + b;
};
function todo(callback:IFunc){
  let v=callback(1,2)
}
todo(function(a:number,b:number):number{
  return a+b;
})

interface IEventFunc{
  (e:Event):void
  // (e:MouseEvent):void
}

function on(el:HTMLElement,evname:string,callback:IEventFunc){}
let div=document.querySelector('div');
if(div){
  on(div,'click',function(e){
    e.target
  })
}

3.6 接口合并

  • 多个同名的接口合并成一个接口
  • 如果合并的接口存在同名的非函数成员,则必须保证他们类型一致,否则编译报错
  • 接口中的同名函数则是采用重载(具体后期函数详解中讲解)
interface Box {
  height: number;
  width: number;
}
interface Box {
  scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10}

四、高级类型

4.0 学习目标

  • 使用 联合类型、交叉类型、字面量类型 来满足更多的标注需求
  • 使用 类型别名、类型推导 简化标注操作
  • 掌握 类型断言 的使用

4.1 联合类型

  • 联合类型也可以称为多选类型,当我们希望标注一个变量为多个类型之一时可以选择联合类型标注,的关系
  • 类似js中的或 |
function css(ele: Element, attr: string, value: string|number) {
  // ...
}
let box = document.querySelector('.box');
// document.querySelector 方法返回值就是一个联合类型
if (box) {
  // ts 会提示有 null 的可能性,加上判断更严谨
  css(box, 'width', '100px');
  css(box, 'opacity', 1);
  css(box, 'opacity', [1,2]);  // 错误
}

4.2 交叉类型

  • 交叉类型也可以称为合并类型,可以把多种类型合并到一起成为一种新的类型,并且 的关系
  • 类似js中的且 &
// 对一个对象进行扩展
interface o1 {x: number, y: string};
interface o2 {z: number};
let o: o1 & o2 = Object.assign({}, {x:1,y:'2'}, {z: 100});
// 下面的也可以
let o:o1&o2={
  x:100,
  y:'a',
  z:300
}

4.3 字面量类型

有的时候,我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,配合联合类型会更有用

function setPosition(
  ele: Element,
  direction: "left" | "top" | "right" | "bottom"
) {}
let box = document.querySelector("div");
// ok
box && setPosition(box, 'bottom');
// error
box && setPosition(box, 'hehe');

4.4 类型别名

  • 有的时候类型标注比较复杂,这个时候我们可以类型标注起一个相对简单的名字
  • type 关键字
// 下面这个字面量类型是可以与基础类型等混合在一起的,但是不建议这么做,比如说设置一个'center'
type dir1 = 'left' | 'top' | 'right' | 'bottom' | string;
type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
// ...
}

使用类型别名定义函数类型

这里需要注意一下,如果使用 type 来定义函数类型,和接口有点不太相同

type callback = (a: string) => string;
let fn: callback = function(a) {return 'a'};
// 或者直接
let fn: (a: string) => string = function(a) {}

interface与type的区别

interface
  • 只能描述 object / class / function 的类型
  • 同名 interface 自动合并,利于扩展
type
  • 不能重名
  • 能描述所有数据

4.5 类型推导

  • 每次都显式标注类型会比较麻烦,TypeScript 提供了一种更加方便的特性:类型推导
  • TypeScript 编译器会根据当前上下文自动的推导出对应的类型标注,这个过程发生在
    • 初始化变量
    • 设置函数默认参数值
    • 返回函数值
// 自动推断 x 为 number
let x = 1;
// 不能将类型“"a"”分配给类型“number”
x = 'a';
// 函数参数类型、函数返回值会根据对应的默认值和返回值进行自动推断
function fn(a = 1) {return a * a}

4.6 类型断言

断言只是一种预判,并不会数据本身产生实际的作用,即:类似转换,但并非真的转换了

有的时候,我们可能标注一个更加精确的类型(缩小类型标注范围),比如:

let img = document.querySelector('#img');

  • img 的类型为 Element
  • Element 类型其实只是元素类型的通用类型
  • 如果去访问 src 这个属性是有问题的
  • 要把它的类型标注得更为精确:HTMLImageElement 类型
  • 这个时候,就可以使用类型断言,它类似于一种 类型转换
let img = <HTMLImageElement>document.querySelector('#img');
// 或者
let img = document.querySelector('#img') as HTMLImageElement;

let img = document.querySelector("#img");
if (img) {
    // img.src	// 会报错,因为src不是Element里的通用属性
    // 下面两种方法都可以
    (<HTMLImageElement>img).src
    (img as HTMLImageElement).src
}

五、函数详解

5.0 学习目标

  • 掌握 TypeScript 中的函数类型标注
  • 函数可选参数和参数默认值
  • 剩余参数
  • 函数中的 this
  • 函数重载

5.1函数的标注

  • 一个函数的标注包含:
    • 参数
    • 返回值
function fn(a: string): string {return ''};

interface ICallBack {
    (a: string): string;
}
let fn: ICallBack = function(a) {return ''};

// 只有这一个是箭头函数
let fn: (a: string) => string = function(a) {return ''};

// 这里也是箭头函数
type callback = (a: string) => string;
let fn: callback = function(a) {return ''};

5.2 可选参数和默认参数

可选参数

通过参数名后面添加 ? 来标注该参数是可选的

let div = document.querySelector('div');
function css(el: HTMLElement, attr: string, val?: any) {
}
// 设置
div && css( div, 'width', '100px' );
// 获取
div && css( div, 'width' );

默认参数

  • 还可以给参数设置默认值
    • 有默认值的参数也是可选的
    • 设置了默认值的参数可以根据值自动推导类型
function sort(items: Array<number>, order = 'desc') {}
sort([1,2,3]);
// 也可以通过联合类型来限制取值
function sort(items: Array<number>, order:'desc'|'asc' = 'desc') {}
// ok
sort([1,2,3]);
// ok
sort([1,2,3], 'asc');
// error
sort([1,2,3], 'abc');

剩余参数

剩余参数是一个数组,所以标注的时候一定要注意

interface IObj {
    [key:string]: any;
}
function merge(target: IObj, ...others: Array<IObj>) {
    return others.reduce( (prev, currnet) => {
        prev = Object.assign(prev, currnet);
        return prev;
    }, target );
}

// 上面的比较复杂了,直接assign即可
function merge(target:IObj,...others:Array<IObj>){
    return Object.assign(target,...others);
}
let newObj = merge({x: 1}, {y: 2}, {z: 3});

5.3 函数中的this

无论是 JavaScript 还是 TypeScript ,函数中的 this 都是我们需要关心的,那函数中 this 的类型该如何进行标注呢

  • 普通函数
  • 箭头函数

普通函数

  • 对于普通函数而言, this 是会随着调用环境的变化而变化的,所以默认情况下,普通函数中的 this被标注为 any
  • 可以在函数的第一个参数位(它不占据实际参数位置)上显式的标注 this 的类型
interface T {
    a: number;
    fn: (x: number) => void;
}
let obj1:T = {
    a: 1,
    fn(x: number) {
        //any类型
        console.log(this);
        // 下面的也会报错
        (<t>this).b
    }
}
let obj2:T = {
    a: 1,
    fn(this: T, x: number) {
        //通过第一个参数位标注 this 的类型,它对实际参数不会有影响
        console.log(this);
    }
}
obj2.fn(1);

箭头函数

  • 箭头函数的 this 不能像普通函数那样进行标注
  • 它的 this 标注类型取决于它所在的作用域 this的标注类型
interface T {
    a: number;
    fn: (x: number) => void;
    fnn:(x:number)=>void;
}
let obj2: T = {
    a: 2,
    fn(this: T) {
        return () => {
            // T
            console.log(this);
        }
    },
    fnn(this:Window,x:number){
        return ()=>{
            // Window
            this
        }
    }
}

5.4 函数重载

有的时候,同一个函数会接收不同类型的参数返回不同类型的返回值,可以使用函数重载来实现

  • 下面的例子,创造了多个同名函数,但是函数后面没有{}
  • 最后跟了一个可选范围比较大(any)的同名函数
  • 这样在匹配的时候可以具体匹配什么
  • noImplicitAny不能设置为true,会报错
function showOrHide(ele: HTMLElement, attr: string, value:
                     'block'|'none'|number) {
    //
}
let div = document.querySelector('div');
if (div) {
    showOrHide( div, 'display', 'none' );
    showOrHide( div, 'opacity', 1 );
    // error,这里是有问题的,虽然通过联合类型能够处理同时接收不同类型的参数,但是多个参数之
    间是一种组合的模式,我们需要的应该是一种对应的关系
    showOrHide( div, 'display', 1 );
}

看一下函数重载

function showOrHide(ele: HTMLElement, attr: 'display', value: 'block'|'none');
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number);
// 注意上面两个函数是没有{}的
function showOrHide(ele: HTMLElement, attr: string, value: any) {
    ele.style[attr] = value;
}
let div = document.querySelector('div');
if (div) {
    showOrHide( div, 'display', 'none' );
    showOrHide( div, 'opacity', 1 );
    // 通过函数重载可以设置不同的参数对应关系
    showOrHide( div, 'display', 1 );
}

重载函数类型只需要定义结构,不需要实体,类似接口

interface PlainObject {
    [key: string]: string|number;
}
// attr:PlainObject  ,这个attr就变成了个对象
function css(ele: HTMLElement, attr: PlainObject);
function css(ele: HTMLElement, attr: string, value: string|number);
function css(ele: HTMLElement, attr: any, value?: any) {
    if (typeof attr === 'string' && value) {
        ele.style[attr] = value;
    }
    if (typeof attr === 'object') {
        for (let key in attr) {
            ele.style[key] = attr[key];
        }
    }
}
let div = document.querySelector('div');
if (div) {
    css(div, 'width', '100px');
    css(div, {width: '100px'});
    // error,如果不使用重载,这里就会有问题了
    css(div, 'width');
}

六、面向对象编程

6.0 学习目标

  • 掌握面向对象编程中类的基本定义与语法
  • 学会使用类修饰符与寄存器
  • 理解并掌握类的实例成员与类的静态成员的区别与使用
  • 理解类与接口的关系,并熟练使用它们
  • 了解类(构造函数)类型与对象类型的区别

6.1 类

  • 面向对象编程中一个重要的核心就是: 类
  • 使用面向对象的方式进行编程的时候,通常会首先去分析具体要实现的功能,把特性相似的抽象成一个一个的类
  • 然后通过这些类实例化出来的具体对象来完成具体业务需求

类的基础

在类的基础中,包含下面几个核心的知识点,也是 TypeScriptEMCAScript2015+ 在类方面共有的一些特性

  • class 关键字
  • 构造函数: constructor
  • 成员属性定义
  • 成员方法
  • this关键字

除了以上的共同特性以外,在 TypeScript 中还有许多 ECMAScript 没有的,或当前还不支持的一些特性,如:抽象

class

通过 class 就可以描述和组织一个类的结构,语法:

// 通常类的名称我们会使用 大坨峰命名 规则,也就是 (单词)首字母大写
class User {
 // 类的特征都定义在 {} 内部
}

构造函数

通过 class 定义了一个类以后,可以通过 new 关键字来调用该类从而得到该类型的一个具体对象:也就是实例化

为什么类可以像函数一样去调用呢,其实执行的并不是这个类,而是类中包含的一个特殊函数:构造函数 - constructor

class User {
    constructor() {
        console.log('实例化...')
    }
}
let user1 = new User;

  • 默认情况下,构造函数是一个空函数
  • 构造函数会在类被实例化的时候调用
  • 我们定义的构造函数会覆盖默认构造函数
  • 如果在实例化(new)一个类的时候无需传入参数,则可以省略 ()
  • 构造函数 constructor 不允许有 return 和返回值类型标注的(因为要返回实例对象)

通常情况下,会把一个类实例化的时候的初始化相关代码写在构造函数中,比如对类成员属性的初始化赋值

成员属性与方法定义

class User {
    // 这种方法过于繁琐,见下方public
    id: number;
    username: string;
    constructor(id: number, username: string) {
        this.id = id;
        this.username = username;
    }
    postArticle(title: string, content: string): void {
        console.log(`发表了一篇文章: ${title}`)
    }
}
let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');

this关键字

在类内部,我们可以通过 this 关键字来访问类的成员属性和方法

class User {
    id: number;
    username: string;
    postArticle(title: string, content: string): void {
        // 在类的内部可以通过 `this` 来访问成员属性和方法
        console.log(`${this.username} 发表了一篇文章: ${title}`)
    }
}

构造函数参数属性

  • 因为在构造函数中对类成员属性进行传参赋值初始化是一个比较常见的场景
  • 所以 ts 提供了一个简化操作:给构造函数参数添加修饰符来直接生成成员属性
  • public 就是类的默认修饰符,表示该成员可以在任何地方进行读写操作
    • 添加了public修饰符以后
      • 给当前类添加同名的成员属性
        • constructor外面的idusername就不用进行标注了
      • 类实例化的时候,会把传入的参数值赋值给对应的成员属性
        • constructor内的this.id=id也可以省略
  • protectedprivatereadonly也会让其变为类的属性,也就是可以通过this来调用
    • 但是上面的都有作用域范围
class User {
    constructor(
    	public id: number,
     	public username: string
    ) {
        // 可以省略初始化赋值
    }
    postArticle(title: string, content: string): void {
        console.log(`${this.username} 发表了一篇文章: ${title}`)
    }
}
let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');

6.2 继承

ts 中,也是通过 extends 关键字来实现类的继承

class VIP extends User {}

super关键字

在子类中,我们可以通过 super 来引用父类

  • 如果子类没有重写构造函数,则会在默认的 constructor 中调用 super()
  • 如果子类有自己的构造函数,则需要在子类构造函数中显示的调用父类构造函数 : super(//参数) ,否则会报错
  • 在子类构造函数中只有在 super(//参数) 之后才能访问 this,父类的属性
    • 写在super之前就不行了
  • 在子类中,可以通过 super 来访问父类的成员属性和方法
    • 父类的属性用this即可
    • 父类的方法是否被重写
      • 重写了:super.父类的方法
      • 没有重写:this.父类的方法
  • 通过 super 访问父类的的同时,会自动绑定上下文对象为当前子类 this
class VIP extends User {
    constructor(
        id: number,
        username: string,
        public score = 0
    ) {
        super(id, username);
        console.log(this.id);
    }
    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }
}
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
vip1.postAttachment('1.png');

方法的重写与重载

默认情况下,子类成员方法集成自父类,但是子类也可以对它们进行重写和重载

  • 重写
    • 重新实现了父类的方法:参数的个数和参数的类型一致
  • 重载
    • 参数个数、参数类型不同
  • 子类中重写了父类的方法,还要用父类的方法
    • super.父类的方法()
class VIP extends User {
    constructor(
        id: number,
        username: string,
        public score = 0
    ) {
        super(id, username);
    }
    // postArticle 方法重写,覆盖
    postArticle(title: string, content: string): void {
        this.score++;
        // 这个文档里竟然不认``,确切说是不认${},确切说,不知道什么问题
		console.log(`${this.username}发表了一篇文章:${title},积分:${this.score}`)
	}
    postAttachment(file: string): void {
    	console.log(`${this.username} 上传了一个附件: ${file}`)
    }
}
// 故意多写一个省的下面变色
`
// 具体使用场景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
class VIP extends User {
    constructor(
    id: number,
     username: string,
     public score = 0
    ) {
        super(id, username);
    }
    // 参数个数,参数类型不同:重载
    postArticle(title: string, content: string): void;
    postArticle(title: string, content: string, file: string): void;
    postArticle(title: string, content: string, file?: string) {
        // super关键字调用父类方法
        super.postArticle(title, content);
        if (file) {
            this.postAttachment(file);
        }
    }

    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }
}
// 具体使用场景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
vip1.postArticle('标题', '内容', '1.png');

6.3 修饰符

  • 有的时候,我们希望对类成员(属性、方法)进行一定的访问控制,来保证数据的安全

  • 通过 类修饰符 可以做到这一点,目前 TypeScript 提供了四种修饰符:

    • public:公有,默认
    • protected:受保护
      • 可以访问,但是不能外部修改
    • private:私有
      • 外部包括子类不能访问也不可修改
      • 自身可以修改
    • readonly:只读
      • 可以访问,但是一旦确定不能修改
  • 倒不是说就不能通过外面的数据修改内部的属性,比如说protected、private等

    • 可以通过set,比如
    class User{
        constructor(
        	private _password:string
        ){}
        set password(password:string){
            this._password=password
        }
    }
    let p1=new User('abc')
    console.log(p1);		// {_password:'abc'}
    p1.password='bdc'
    console.log(p1);		// {_password:'bcd'}
    
    

public修饰符

这个是类成员的默认修饰符,它的访问级别为:

  • 自身
  • 子类
  • 类外

修改级别为:

  • 自身
  • 子类
  • 类外

protected修饰符

它的访问级别为:

  • 自身
  • 子类

修改级别:

  • 自身
  • 子类

private修饰符

它的访问级别为:

  • 自身

修改级别:

  • 自身

readonly修饰符

只读修饰符只能针对成员属性使用,且必须在声明时或构造函数里被初始化,它的访问级别为:

  • 自身
  • 子类
  • 类外

修改级别:

class User {
    constructor(
    // 可以访问,但是一旦确定不能修改
    readonly id: number,
     // 可以访问,但是不能外部修改
     protected username: string,
     // 外部包括子类不能访问,也不可修改
     private password: string
    ) {
        // ...
    }
    // ...
    protected method(){}
}
let user1 = new User(1, 'zMouse', '123456');

6.4 寄存器

  • 有的时候,我们需要对类成员 属性 进行更加细腻的控制,就可以使用 寄存器 来完成这个需求
  • 通过寄存器 ,可以对类成员属性的访问进行拦截并加以控制,更好的控制成员属性的设置和访问边界
  • 寄存器分为两种:
    • getter
    • setter

getter

访问控制器,当访问指定成员属性时调用

setter-组件

  • 函数式组件
  • 类式组件
  • props 与 state
  • 组件通信
  • 表单与受控组件

设置控制器,当设置指定成员属性时调用

class User {
    constructor(
        readonly _id: number,
        readonly _username: string,
        private _password: string
    ) {
    }
    public set password(password: string) {
        if (password.length >= 6) {
            this._password = password;
        }
    }
    public get password() {
        return '******';
    }
    // ...
}

6.5 静态成员

  • 成员属性和方法都是实例对象的
  • 有的时候需要给类本身添加成员
  • 区分某成员是静态还是实例的:
    • 该成员属性或方法是类型的特征还是实例化对象的特征
    • 如果一个成员方法中没有使用或依赖 this ,那么该方法就是静态的
type IAllowFileTypeList = 'png'|'gif'|'jpg'|'jpeg'|'webp';
class VIP extends User {
    // static 必须在 readonly 之前
    static readonly ALLOW_FILE_TYPE_LIST: Array<IAllowFileTypeList> =
    ['png','gif','jpg','jpeg','webp'];
    constructor(
    id: number,
     username: string,
     private _allowFileTypes: Array<IAllowFileTypeList>
    ) {
        super(id, username);
    }
    info(): void {
        // 类的静态成员都是使用 类名.静态成员 来访问
        // VIP 这种类型的用户允许上传的所有类型有哪一些
        console.log(VIP.ALLOW_FILE_TYPE_LIST);
        // 当前这个 vip 用户允许上传类型有哪一些
        console.log(this._allowFileTypes);
    }
}
let vip1 = new VIP(1, 'zMouse', ['jpg','jpeg']);
// 类的静态成员都是使用 类名.静态成员 来访问
console.log(VIP.ALLOW_FILE_TYPE_LIST);
this.info();

  • 类的静态成员是属于类的,所以不能通过实例对象(包括 this)来进行访问,而是直接通过类名访问(不管是类内还是类外)
  • 静态成员也可以通过访问修饰符进行修饰
  • 静态成员属性一般约定(非规定)全大写

6.6 抽象类

有的时候,一个基类(父类)的一些方法无法确定具体的行为,而是由继承的子类去实现

现在前端比较流行组件化设计,比如 React

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {}
    }
    render() {
        //...
    }
}

根据上面代码,可以大致设计如下类结构

  • 每个组件都一个 props 属性,可以通过构造函数进行初始化,由父级定义
  • 每个组件都一个 state 属性,由父级定义
  • 每个组件都必须有一个 render 的方法
// 泛型
class Component<T1, T2> {
    public state: T2;
    constructor(
    public props: T1
    ) {
        // ...
    }
    render(): string {
        // ...不知道做点啥才好,但是为了避免子类没有 render 方法而导致组件解析错误,父类就用一个默认的 render 去处理可能会出现的错误
    }
}
interface IMyComponentProps {
    title: string;
}
interface IMyComponentState {
    val: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {
    constructor(props: IMyComponentProps) {
        super(props);
        this.state = {
            val: 1
        }
    }
    render() {
        this.props.title;
        this.state.val;
        return `
组件
`
; } }

父类的 render 有点尴尬(当子类没有定义render的时候,调用render实际走的是父类的render方法,并不是预期的效果),更应该从代码层面上去约束子类必须得有 render 方法,否则编码就不能通过

abstract关键字

如果一个方法没有具体的实现方法,则可以通过 abstract 关键字进行修饰

abstract class Component<T1, T2> {
    public state: T2;
    constructor(
    public props: T1
    ) {
    }
    public abstract render(): string;
}

  • 使用抽象类有一个好处:
    • 约定了所有继承子类的所必须实现的方法,使类的设计更加的规范
  • 使用注意事项:
    • abstract 修饰的方法不能有方法体,也就是没有{}
    • 如果一个类有抽象方法,那么该类也必须为抽象的
    • 如果一个类是抽象的,那么就不能使用 new 进行实例化(因为抽象类表名该类有未实现的方法,所以不允许实例化)
      • 不能new
    • 如果一个子类继承了一个抽象类,那么该子类就必须实现抽象类中的所有抽象方法,否则该类还得声明为抽象的
      • 抽象类中的render()方法是抽象的,子类如果没有定义render()方法会报错

6.7 类与接口

  • 通过接口,可以为对象定义一种结构和契约
  • 还可以把接口与类进行结合,通过接口,让类去强制符合某种契约
  • 从某个方面来说,当一个抽象类中只有抽象的时候,它就与接口没有太大区别了
  • 这个时候,我们更推荐通过接口的方式来定义契约
    • 抽象类编译后还是会产生实体代码,而接口不会
    • TypeScript 只支持单继承,即一个子类只能有一个父类,但是一个类可以实现多个接口
    • 接口不能有实现,抽象类可以

implements

契约

在一个类中使用接口并不是使用 extends 关键字,而是 implements

  • 与接口类似,如果一个类 implements 了一个接口,那么就必须实现该接口中定义的契约
  • 多个接口使用 , 分隔
  • implementsextends 可同时存在
interface ILog {
    getInfo(): string;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog {
    constructor(props: IMyComponentProps) {
        super(props);
        this.state = {
            val: 1
        }
    }
    render() {
        this.props.title;
        this.state.val;
        return `
组件
`
; } // 如果没有这个方法会报错 getInfo() { return `组件:MyComponent,props:${this.props},state:${this.state}`; } }

实现多个接口

interface ILog {
    getInfo(): string;
}
interface IStorage {
    save(data: string): void;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog, IStorage {
    constructor(props: IMyComponentProps) {
        super(props);
        this.state = {
            val: 1
        }
    }
    render() {
        this.props.title;
        this.state.val;
        return `
组件
`
; } getInfo(): string { return `组件:MyComponent,props:${this.props},state:${this.state}`; } save(data: string) { // ... 存储 } }

接口也可以继承

interface ILog {
  getInfo(): string;
}
// 继承以后IStorage既有getInfo()又有save()
interface IStorage extends ILog {
  save(data: string): void;
}

6.8 类与对象类型

当在 TypeScript 定义一个类的时候,其实同时定义了两个不同的类型

  • 类类型(构造函数类型)
  • 对象类型

首先,对象类型好理解,就是 new 出来的实例类型

那类类型是什么,我们知道 JavaScript 中的类,或者说是 TypeScript 中的类其实本质上还是一个函数,

当然我们也称为构造函数,那么这个类或者构造函数本身也是有类型的,这个类型就是类的类型

class Person {
    // 属于类的
    static type = '人';
    // 属于实例的
    name: string;
    age: number;
    gender: string;
    // 类的构造函数也是属于类的
    constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    public eat(): void {
        // ...
    }
}
let p1 = new Person('zMouse', 35, '男');
p1.eat();
Person.type;

上面例子中,有两个不同的数据

  • Person 类(构造函数)
  • 通过 Person 实例化出来的对象 p1

对应的也有两种不同的类型

  • 实例的类型( Person
  • 构造函数的类型( typeof Person

用接口的方式描述如下

interface Person {
    name: string;
    age: number;
    gender: string;
    eat(): void;
}
interface PersonConstructor {
    // new 表示它是一个构造函数
    new (name: string, age: number, gender: '男'|'女'): Person;
    type: string;
}

在使用的时候要格外注意

  • 主要是传入的是实例,还是可以new的
function fn1(arg: Person /*如果希望这里传入的Person 的实例对象*/) {
    arg.eat();
}
fn1( new Person('', 1, '男') );		// 这里的new是上面的class类

// 下面的typeof Person返回的是class类的构造函数
function fn2(arg: typeof Person /*如果希望传入的Person构造函数*/) {
    new arg('', 1, '男');
}
// 上面的fn2与下面的fn3相同,写法不同
function fn3(arg:PersonConstructor){
    new arg('', 1, '男');
}
fn2(Person);

七、类型系统深入

7.0 学习目标

  • 理解什么是类型保护,并能使⽤类型保护去优化代码逻辑
  • 熟悉类型操作符的使⽤,进⼀步理解类型系统

7.1 类型保护

  • 通常在 JavaScript 中通过判断来处理⼀些逻辑
  • TypeScript 中这种条件语句块还有另外⼀个特性:根据判断逻辑的结果,缩⼩类型范围(有点类似断⾔)
  • 这种特性称为 类型保护 ,触发条件:
    • 逻辑条件语句块:if、else、elseif
    • 特定的⼀些关键字:typeof、instanceof、in……

typeof

typeof 可以返回某个数据的类型,在 TypeScriptifelse 代码块中能够把typeof 识别为类型保护,推断出适合的类型

function fn(a: string|number) {
    // error,不能保证 a 就是字符串
    a.substring(1);
    if (typeof a === 'string') {
        // ok
        a.substring(1);
    } else {
        // ok
        a.toFixed(1);
    }
}

instanceof

typeof 类似的, instanceof 也可以被 TypeScript 识别为类型保护

function fn(a: Date|Array<any>|RegExp) {
    if (a instanceof Array) {
        a.push(1);
    } else if(a instanceof Date){
        a.getFullYear();
    }else if(a instanceof RegExp){
        
    }
}

in

in 也是如此

interface IA {
    x: string;
    y: string;
}
interface IB {
    a: string;
    b: string;
}
function fn(arg: IA | IB) {
    if ('x' in arg) {
        // ok
        arg.x;
        // error
        arg.a;
    } else {
        // ok
        arg.a;
        // error
        arg.x;
    }
}

字面量类型保护

如果类型为字⾯量类型,那么还可以通过该字⾯量类型的字⾯值进⾏推断

interface IA {
    type: 'IA';
    x: string;
    y: string;
}
interface IB {
    type: 'IB';
    a: string;
    b: string;
}
function fn(arg: IA | IB) {
    if (arg.type === 'IA') {
        // ok
        arg.x;
        // error
        arg.a;
    } else {
        // ok
        arg.a;
        // error
        arg.x;
    }
}

自定义类型保护

有的时候,以上的⼀些⽅式并不能满⾜⼀些特殊情况,则可以⾃定义类型保护规则

data is Element[]|NodeList 是⼀种类型谓词,格式为: xx is XX ,返回这种类型的函数就可以被 TypeScript 识别为类型保护

function canEach(data: any): data is Element[]|NodeList {
    return data.forEach !== undefined;
}
function fn2(elements: Element[]|NodeList|Element) {
    if ( canEach(elements) ) {
        elements.forEach((el: Element)=>{
            el.classList.add('box');
        });
    } else {
        elements.classList.add('box');
    }
}

7.2 类型操作

TypeScript 提供了⼀些⽅式来操作类型这种数据,但是需要注意的是,类型数据只能作为类型来使⽤,⽽不能作为程序中的数据,这是两种不同的数据,⼀个⽤在编译检测阶段,⼀个⽤于程序执⾏阶段

typeof

TypeScript 中, typeof 有两种作⽤

  • 获取数据的类型
  • 捕获数据的类型
let str1 = 'kaikeba';
// 如果是 let ,把 'string' 作为值
let t = typeof str1;
// 如果是 type,把 'string' 作为类型
type myType = typeof str1;
let str2: myType = '开课吧';
let str3: typeof str1 = '开课吧';

let obj={
    name:'aa',
    age:30
}
// 用type,a的值是{name:string,age:number}
type a = typeof obj;
// 用let,a的值是Object
let a = typeof obj;

keyof

获取类型的所有 key 的集合

集合的形式为 ‘aa’ | ‘bb’ | ‘cc’ | …

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

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string | number


class Person {
  name: string = "Semlinker";
}

let sname: keyof Person;
sname = "name";

let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"

type P1 = Person["name"];  // string
type P2 = Person["name" | "age"];  // string | number
type P3 = string["charAt"];  // (pos: number) => string
type P4 = string[]["push"];  // (...items: string[]) => number
type P5 = string[][0];  // string


interface Person {
    name: string;
    age: number;
};
type personKeys = keyof Person;
// 等同:type personKeys = "name" | "age"
// 
let p1 = {
    name: 'zMouse',
    age: 35
}
function getPersonVal(k: personKeys) {
    return p1[k];
}
type a = typeof p1;
// 上面a的值是{name:string,age:number}
function getPersonVal(k:keyof typeof p1){
    return p1[k]
}
/**
等同:
function getPersonVal(k: 'name'|'age') {
return p1[k];
}
*/
getPersonVal('name'); //正确
getPersonVal('gender'); //错误

// 另外一个范例
type Todo = {
  id: number;
  text: string;
  done: boolean;
}

const todo: Todo = {
  id: 1,
  text: "Learn TypeScript keyof",
  done: false
}

function prop<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean

in

针对类型进⾏操作的话,内部使⽤的 for…in 对类型进⾏遍历

下面例子的目的是把age的number类型转化成string类型

也就是说遍历原本的类型,都转化成统一类型

interface Person {
    name: string;
    age: number;
}
type personKeys = keyof Person;
type newPerson = {
    [k in personKeys]: number;
    /**
等同 [k in 'name'|'age']: number;
也可以写成
[k in keyof Person]: number;
*/
}
/**
type newPerson = {
name: number;
age: number;
}
*/

注意: in 后⾯的类型值必须是 string 或者 number 或者 symbol

7.3 类型兼容

TypeScript 的类型系统是基于结构⼦类型的,它与名义类型(如:java)不同(名义类型的数据类型兼容性或等价性是通过明确的声明或类型的名称来决定的)。这种基于结构⼦类型的类型系统是基于组成结构的,只要具有相同类型的成员,则两种类型即为兼容的。

class Person {
    name: string;
    age: number;
}
class Cat {
    name: string;
    age: number;
}
function fn(p: Person) {
    p.name;
}
let xiaohua = new Cat();
// ok,因为 Cat 类型的结构与 Person 类型的结构相似,所以它们是兼容的
fn(xiaohua);

interface IFly{
  fly():void
}
class Person implements IFly{
  name:string;
  age:number;
  study(){}
  fly(){}
}
class Cat{
  name:string;
  age:number;
  catchMouse(){}
}
// 没有定义IFly的时候,这里放Person或者Cat,如果有对应的属性或者方法还好,如果没有就出问题了
function fn2(arg:IFly){
  arg.fly();
}
fn2(pp)
// error 没有fly方法
fn2(cc)

八、泛型

8.0 学习目标

  • 理解泛型概念与使用场景
  • 在函数、类、接口中使用泛型

8.1 为什么要使用泛型

许多时候,标注的具体类型并不能确定,比如一个函数的参数类型

function getVal(obj, k) {
    return obj[k];
}

  • 上面的函数,我们想实现的是获取一个对象指定的 k 所对应的值
  • 那么实际使用的时候,obj 的类型是不确定的,自然 k 的取值范围也是不确定的
  • 它需要我们在具体调用的时候才能确定,这个时候这种定义过程不确定类型的需求就可以通过泛型来解决

8.2 泛型的使用-函数

所谓的泛型,就是给可变(不定)的类型定义变量(参数), <> 类似 ()

function getVal<T>(obj: T, k: keyof T) {
    return obj[k];
}
let obj1={
  x:1,
  y:2
}
let obj2={
  username:'zmouse',
  age:35
}
// 这里的typeof obj1实际上等于type obj=typeof obj1  getVal
getVal<typeof obj1>(obj1,'x')
getVal<typeof obj2>(obj2,'age')

8.3 泛型类

在面向对象章节中,我们曾经给大家讲过一个基于泛型使用的例子:模拟组件

abstract class Component<T1, T2> {
    props: T1;
    state: T2;
    constructor(props: T1) {
        this.props = props;
    }
    abstract render(): string;
}
interface IMyComponentProps {
    val: number;
}
interface IMyComponentState {
    x: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {
    constructor(props: IMyComponentProps) {
        super(props);
        this.state = {
            x: 1
        }
    }
    render() {
        this.props.val;
        this.state.x;
        return '';
    }
}
let myComponent = new MyComponent({val: 1});
myComponent.render();

8.4 泛型接口

还可以在接口中使用泛型

后端提供了一些接口,用以返回一些数据,依据返回的数据格式定义如下接口:

interface IResponseData {
    code: number;
    message?: string;
    data: any;
}

根据接口,我们封装对应的一些方法

function getData(url: string) {
    return fetch(url).then(res => {
        return res.json();
    }).then( (data: IResponseData) => {
        return data;
    });
}

但是,我们会发现该接口的 data 项的具体格式不确定,不同的接口会返回的数据是不一样的,当我们想根据具体当前请求的接口返回具体 data 格式的时候,比较麻烦了,因为 getData 并不清楚你调用的具体接口是什么,对应的数据又会是什么样的
这个时候我们可以对 IResponseData 使用泛型

interface IResponseData<T> {
    code: number;
    message?: string;
    data: T;
}
function getData<U>(url: string) {
    return fetch(url).then(res => {
        return res.json();
    }).then( (data: IResponseData<U>) => {
        return data;
    });
}

定义不同的数据接口

// 用户接口
interface IResponseUserData {
    id: number;
    username: string;
    email: string;
}
// 文章接口
interface IResponseArticleData {
    id: number;
    title: string;
    author: IResponseUserData;
}

调用具体代码

(async function(){
    let user = await getData<IResponseUserData>('');
    if (user.code === 1) {
        console.log(user.message);
    } else {
        console.log(user.data.username);
    }
    let articles = await getData<IResponseArticleData>('');
    if (articles.code === 1) {
        console.log(articles.message);
    } else {
        console.log(articles.data.id);
        console.log(articles.data.author.username);
    }
})();

九、模块系统

9.0 学习目标

  • 了解模块化的演进过程
  • 不同的模块化标准的使用与它们之间的差异性
  • 熟悉 TypeScript 模块化的使用以及与其它模块化系统的差异
  • 熟悉 TypeScript 模块化与其它模块化系统之间的编译与转换
  • 学会使用 TypeScript 模块化进行项目开发

9.1 模块化

模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能

从基于 Node.js 的服务端 commonjs 模块化,到前端基于浏览器的 AMDCMD 模块化,再到 ECMAScript2015 开始原生内置的模块化, JavaScript 的模块化方案和系统日趋成熟。

TypeScript 也是支持模块化的,而且它的出现要比 ECMAScript模块系统标准化要早,所以在 TypeScript 中即有对 ECMAScript 模块系统的支持,也包含有一些自己的特点

9.2 模块化历程

  • CommonJS
  • AMD
  • UMD
  • ESM

无论是那种模块化规范,重点关注:保证模块独立性的同时又能很好的与其它模块进行交互

  • 如何定义一个模块与模块内部私有作用域
  • 通过何种方式导出模块内部数据
  • 通过何种方式导入其它外部模块数据

9.3 基于服务端、桌面端的模块化

CommonJS

在早期,对于运行在浏览器端的 JavaScript 代码,模块化的需求并不那么的强烈,反而是偏向 服务端、桌面端 的应用对模块化有迫切的需求(相对来说,服务端、桌面端程序的代码和需求要复杂一些)。CommonJS 规范就是一套偏向服务端的模块化规范,它为非浏览器端的模块化实现制定了一些的方案和标准,NodeJS 就采用了这个规范。

独立模块作用域

一个文件就是模块,拥有独立的作用域

导出模块内部数据

通过 module.exportsexports 对象导出模块内部数据

  • 原理
    • 每个模块(文件中)都有一个exports对象,这个对象默认是个空对象
      • 每个模块都是一个module的实例对象
      • 所以commonjs中实际导出的是module对象
      • 但是commonjs内部默认做了module.exports = exports
        • 这个赋值的操作是在模块最顶层,后续改exports不会影响到module.exports的值
      • 等于说module.exports和exports指向同一个对象
      • 后面module.exports = {} 就把指向变了
    • require这个函数,想办法拿到参数那个模块的exportsmodule对象
      • 导入的时候哪怕导入的是个对象,整个js文件也从上往下运行了一遍
      • 加载过程是同步的,也就是先导入,再执行自身的内容,这就是为什么要放在最上面
      • 模块被多次引入时,会缓存,最终只加载(运行)一次
        • 哪怕main.js里 加载bar和foo,foo里加载bar,实际bar只加载了一次
        • commonjs中的module有一个loaded属性,加载过以后就变成了true
    • 两个模块之间的exports依然保留着引用关系
    • 所以commonjs的本质就是对象的引用赋值
    • 加载过程是同步的,也就是先导入,再执行自身的内容,这就是为什么要放在最上面
    • 加载的时候采用的是深度优先搜索
// a.js
let a = 1;
let b = 2;

setTimeout(()=>{
    a=20
}.1000)

setTimeout(()=>{
    console.log(b);		// aaa
},2000)

// or 实际上是把exports这个对象导出去了,那么b.js在require的时候导入的a就是这个exports对象
exports.x = a;
exports.y = b;

// 注意,这个定时器是基于module.exports = exports ,下面module.exports就改变指向了
setTimeout( () => {
    module.exports.name = 'bb'
    console.log(exports.name);		// 'bb'
},3000)

// module.exports = exports 变成了module.exports = {}
module.exports = {
  x: a,
  y: b
}


导入外部模块数据

通过 require 函数导入外部模块数据

查找顺序:

  • 情况一:X是一个核心模块,比如path、http
    • 直接返回核心模块,并且停止查找
  • 情况二:X是以 ./ 或 …/ 或 /(根目录)开头的
    • 第一步:将X当做一个文件在对应的目录下查找
      • 如果有后缀名,按照后缀名的格式查找对应的文件
      • 如果没有后缀名,会按照如下顺序
        • 直接查找文件X
        • 查找X.js文件
        • 查找X.json文件
        • 查找X.node文件
    • 第二步:没有找到对应的文件,将X作为一个目录
      • 查找目录下面的index文件
        • 查找X/index.js文件
        • 查找X/index.json文件
        • 查找X/index.node文件
  • 情况三:直接是一个X(没有路径),并且X不是一个核心模块
    • 详细的见下方node里面的非相对导入,会逐层往上找node_modules中的
  • 如果上面的路径中都没有找到,那么报错:not found
// b.js
let a = require('./a');
a.x;
a.y;
setTimeout(()=>{
    console.log(a.x);		// 20
},2000)

setTimeout(()=>{
    a.y='aaa'
},1000)

9.4 基于浏览器的模块化

AMD

因为 CommonJS 规范一些特性(基于文件系统,同步加载),它并不适用于浏览器端,所以另外定义了适用于浏览器端的规范

异步模块定义:

AMD(Asynchronous Module Definition)

https://github.com/amdjs/amdjs-api/wiki/AMD

浏览器并没有具体实现该规范的代码,我们可以通过一些第三方库来解决

requireJS

https://requirejs.org/

// 1.html
// data-main加载的是程序的入口文件,也就是第一个JS文件
<script data-main="js/a" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>

独立模块作用域

通过一个 define 方法来定义一个模块,在该方法内部模拟模块独立作用域

// b.js
define(function() {
  // 模块内部代码
})

导出模块内部数据

通过 return 导出模块内部数据

// b.js
define(function() {
    let a = 1;
    let b = 2;
    return {
        x: a,
        y: b
    }
})

导入外部模块数据

通过前置依赖列表导入外部模块数据

// a.js
// 定义一个模块,并导入 ./b 模块
define(['./b'], function(m2) {
	console.log(m2.x);	// m2就是上面的对象{x:a,y:b}
})

requireJSCommonJS 风格

require.js 也支持 CommonJS 风格的语法

CMD

  • 跟amd的区别是导入的时机和导出的方式

导出模块内部数据

// b.js
define(function(require, exports, module) {
    let a = 1;
    let b = 2;
    // module.exports===exports
    // 但是用exports导出的话得是  exports.x=a   exports.y=b
    module.exports = {
        x: a,
        y: b
    }
})

导入外部模块数据

// a.js
define(function(require, exports, module) {
    let b = require('./b')
    console.log(b);
})

UMD

严格来说,UMD 并不属于一套模块规范,它主要用来处理 CommonJSAMDCMD 的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行。随着 Node.js 的流行,前端和后端都可以基于 JavaScript 来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD 就是一种解决方式

(function (root, factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        // Node, CommonJS-like
        module.exports = factory();
    }
    else if (typeof define === "function" && define.amd) {
        // AMD 模块环境下
        define(factory);
    } else {
        // 不使用任何模块系统,直接挂载到全局
        root.kkb = factory();
    }
}(this, function () {
    let a = 1;
    let b = 2;

    // 模块导出数据
    return {
        x: a,
        y: b
    }
}));

// 另外一种写法,帮助理解
// 可以跟上面相互补充
M(function(){
    funciton fn1(){
        console.log('fn1');
    }
    return {fn1}
})
(function M(root, factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        // Node, CommonJS-like
        module.exports = factory();
    }
    else if (typeof define === "function" && define.amd) {
        // AMD 模块环境下
        define(factory);
    }else{
        root.f=factory();
    }
}(this.function(){}))

9.5 模块化的大同世界

ESM

ECMAScript2015/ECMAScript6 开始,JavaScript 原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js 也有了支持,所以未来基于 JavaScript 的程序无论是在前端浏览器还是在后端 Node.js 中,都会逐渐的被统一

独立模块作用域

一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式 下,即:'use strict'

script 标签需要声明 type="module"

打开的html页面必须用live server

import和export是关键字,不是对象,也不是函数

你可能感兴趣的:(TS,ts,typescript,javascript)