当前位置:   article > 正文

TypeScript 基础 建议收藏!_学typescript需要学js吗

学typescript需要学js吗

1、TypeScript 简介

1.1、什么是 TypeScript

TypeScript 不是一门全新的语言,TypeScriptJavaScript 的超集,它对 JavaScript 进行了一些规范和补充。所以,学习 TypeScript 需要有 JavaScript 的基础

TypeScript 的特性:

  • TypeScriptJavaScript 的超集,它可以编译成纯 JavaScript
  • TypeScript 基于 ECMAScript 标准进行拓展,支持 ECMAScript 未来提案中的特性,如装饰器、异步功能等。
  • TypeScript 编译的 JavaScript 可以在任何浏览器运行,TypeScript 编译工具可以运行在任何操作系统上。
  • TypeScript 起源于开发较大规模 JavaScript 应用程序的需求。由微软在2012年发布了首个公开版本。

1.2、为什么要使用 TypeScript

静态类型

JavaScript 只会在运行时去做数据类型检查,而 TypeScript 作为静态类型语言,其数据类型是在编译期间确定的,编写代码的时候要明确变量的数据类型。使用 TypeScript 后,就不会出现因为数据类型错误导致的代码运行异常。

常见的 JavaScript 异常有:

Uncaught TypeError: Cannot read property
TypeError: ‘undefined’ is not an object
TypeError: null is not an object
TypeError: Object doesn’t support property
TypeError: ‘undefined’ is not a function
TypeError: Cannot read property ‘length’
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

三大框架支持

TypeScript 之所以能够流行起来并且保持长久的生命力,离不开前端三大框架的支持。

  • AngularTypeScript 最早的支持者,Angular 官方推荐使用 TypeScript 来创建应用。
  • React 虽然经常与 Flow 一起使用,但是对 TypeScript 的支持也很友好。
  • Vue3.0 正式版即将发布,由 TypeScript 编写。

从国内的氛围来看,由前端三大框架引领的 TypeScript 热潮已经涌来,很多招聘要求上也都有了 TypeScript的身影。

兼容 JavaScript

TypeScript 虽然严谨,但没有丧失 JavaScript 的灵活性,TypeScript 非常包容:

  • TypeScript 通过 tsconfig.json 来配置对类型的检查严格程度。
  • 可以把 .js 文件直接重命名为 .ts
  • 可以通过将类型定义为 any 来实现兼容任意类型。
  • 即使 TypeScript 编译错误,也可以生成 JavaScript 文件。

这里先简单介绍下 any 类型,后续会详细讲解。比如一个 string 类型,在赋值过程中类型是不允许改变的:

let brand: string = 'hello'
brand = 1  // Type '1' is not assignable to type 'string'.ts(2322)
代码块12
  • 1
  • 2
  • 3

如果是 any 类型,则允许被赋值为任意类型,这样就跟我们平时写 JavaScript 一样了:

let brand: any = 'hello'
brand = 1 
代码块12
  • 1
  • 2
  • 3

基于上面这些特点,一个熟悉 JavaScript 的工程师,在查阅一些 TypeScript 语法后,即可快速上手 TypeScript

2、安装 TypeScript

2.1、使用 tsc 命令编译 TypeScript 代码

安装命令:

$ npm install -g typescript
# 或使用淘宝镜像
$ cnpm i -g typescript
  • 1
  • 2
  • 3

查看当前 typescript 版本:

$ tsc -v
  • 1

创建 index.ts 文件,示例代码如下:

const msg:string = 'hello'
console.log(msg);
  • 1
  • 2

在当前文件所在目录运行命令:

$ tsc index.ts
  • 1

会生成一个同名的 index.js 文件,然后在使用 node 命令运行 index.js 文件:

$ node index.js
  • 1

运行成功即可以在控制台打印 hello 的内容。

2.2、使用 ts-node 快速编译 TypeScript

安装 ts-node :

$ npm install -g ts-node
# 或
$ cnpm i -g ts-node
  • 1
  • 2
  • 3

使用 ts-node 可以快速运行 .ts 文件,无需先转成 .js 文件后再运行,使用 ts-node 运行 index.ts 文件:

$ ts-node index.ts
  • 1

如果运行 ts-node 命令报错,提示:Error: Cannot find module '@types/node/package.json',需要安装相关依赖:

$ npm install -g tslib @types/node
  • 1

2.3、使用工程化编译方案

创建 ts-project 项目的目录,在当前项目根目录下启动命令行工具,初始化 package.json文件:

$ npm init -y
  • 1

package.json 中入口文件选项改为 scr/index.ts

{
    "main": "src/index.ts"
}
  • 1
  • 2
  • 3

然后使用 tsc 命令进行初始化:

$ tsc --init
  • 1

在项目根目录下就会自动创建一个 tsconfig.json 文件,它指定了用来编译这个项目的根文件和编译选项。

tsconfig.json 配置文件做修改:

{
  "compilerOptions": {
    "target": "ESNext",     /* 支持 ES6 语法 */                      
    "module": "commonjs",                     
    "outDir": "./lib",
    "rootDir": "./src",
    "declaration": true,    /* 生成相应的.d.ts文件 */
    "strict": true, 
    "strictNullChecks": false,
    "noImplicitThis": true
  },
  "exclude": ["node_modules", "lib", "**/*.test.ts"],
  "include": ["src"]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

package.json 文件中,加入 script 命令以及依赖关系:

{
  "name": "ts-project",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.ts",
  "scripts": {
    "tsc": "tsc"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^13.1.1",
    "typescript": "^3.7.4"
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

下载相关依赖:

$ npm install
  • 1

你会看到多了一个 node_modules 文件夹和一个 package-lock.json 文件,node_modules 文件夹是项目的所有依赖包,package-lock.json 文件将项目依赖包的版本锁定,避免依赖包大版本升级造成不兼容问题。

src/index.ts 文件中编写 TypeScript 代码:

export enum TokenType {
  ACCESS = 'accessToken',
  REFRESH = 'refreshToken'
}
  • 1
  • 2
  • 3
  • 4

执行编译命令:

$ npm run tsc
  • 1

这时候可以看到多了一个 lib 文件夹,里面的内容就是项目的编译结果了。

lib 文件夹中会生成两个文件:

  • index.d.ts
  • index.js

.d.ts 是声明文件,用于编写第三方类库,通过配置 tsconfig.json 文件中的 declarationtrue ,在编译时可自行生成。

3、TypeScript 基本数据类型

3.1、变量声明

TypeScriptJavaScript 的超集,同 JavaScript 一样,声明变量可以采用下面三个关键字:

  • var
  • let
  • const

详细解释可以查看 ES6 的相关教程。

TypeScript 中变量声明语法:冒号 : 前面是变量名称,后面是变量类型。例如:

let i:number = 1
  • 1

3.2、TypeScript 基本类型

TypeScript 中的类型有:

  • 原始类型
    • boolean
    • number
    • string
    • void
    • null
    • undefined
    • bigint
    • symbol
  • 元组 tuple
  • 枚举 enum
  • 任意 any
  • unknown
  • never
  • 数组 Array
  • 对象 object

布尔类型

表示逻辑值:truefalse,声明一个布尔类型:

const boo: boolean = false
boo = 0 //报错

const done: boolean = Boolean(0)
  • 1
  • 2
  • 3
  • 4

数字类型

表示双精度 64 位浮点值,可以用来表示整数和分数。

let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744;    // 八进制
let decLiteral: number = 6;    // 十进制
let hexLiteral: number = 0xf00d;    // 十六进制
  • 1
  • 2
  • 3
  • 4

字符串类型

一个字符串系列,使用单引号(')或双引号(")来表示字符串类型,反引号(`)来定义多行文本和内嵌表达式。

let name: string = "tom";
let age: number = 5;
let words: string = `我叫 ${ name },今年 ${ age }`;
  • 1
  • 2
  • 3

任意类型

声明为 any 的变量可以赋值任意类型的值。

let str:any = 'hello'
str = 12
str = true
str = [1,2,3,4]
  • 1
  • 2
  • 3
  • 4

任意值是 TypeScript 针对编程时类型不明确的变量使用的一种数据类型,常用于以下三种情况

1、变量的值会动态改变时,比如来自用于的输入,任意类型可以让这些变量跳过编译阶段的类型检查,示例代码如下:

let x: any = 1;    // 数字类型
x = 'I am who I am';    // 字符串类型
x = false;    // 布尔类型
  • 1
  • 2
  • 3

2、改写现有代码时,任意类型允许在编译时可选择地包含或移除类型检查,示例代码如下:

let x: any = 4;
x.ifItExists();    // 正确,ifItExists方法在运行时可能存在,但这里并不会检查
x.toFixed();    // 正确
  • 1
  • 2
  • 3

3、定义存储各种类型数据的数组时,示例代码如下:

let arrayList: any[] = [1, false, 'fine'];
arrayList[1] = 100;
  • 1
  • 2

联合类型

如果想要为一个变量赋值多种指定的类型,在声明变量时可以使用 | 为该变量指定多种联合类型。

let numOrStr:number | string = 'hello'
numOrStr = 12
numOrStr = true // 报错
  • 1
  • 2
  • 3

数组类型

声明变量为数组。

// 在元素类型后面加上[]
let arr: number[] = [1, 2];
arr.push('hello') // 报错

// 或者使用数组泛型
let arr: Array<number> = [1, 2];
arr = [1,2,'hello'] // 报错

//混合元素类型
let list: any[] = ['Sherlock', 1887]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

元组

元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。

let x: [string, number];
x = ['hello', 1];    // 运行正常
x = [1, 'hello'];    // 报错
x = ['hello', 1, true]; // 报错

console.log(x[0]);    // 输出 hello
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

枚举

枚举类型用于定义数值集合。

数字枚举:

//数字枚举,枚举会从0开始,依次为元素赋值
enum Color{
    Blue,
    Red,
    Green
}

console.log(Color.Blue); // 输出 0

//使用数字枚举
let a:Color = Color.Blue
console.log(a) // 输出 0

//反向映射
console.log(Color[0]); // 输出字符串类型的 Blue
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以为数字枚举手动赋值:

enum Color{
    Blue = 10,
    Red,
    Green
}

console.log(Color.Blue); // 输出 10
console.log(Color.Red); // 输出 11

//反向映射
console.log(Color[0]); // 输出 undefined
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

字符串枚举:

enum Color{
    Blue = 'BLUE',
    Red = 'RED',
    Green = 'GREEN'
}

console.log(Color.Blue); // 输出字符串类型的 BLUE
console.log(Color.Red); // 输出字符串类型的 RED

let str = 'RED'
console.log(str === Color.Red); // 输出 true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

常量枚举

没有使用常量枚举之前编译的效果:

// index.ts
enum Color{
    Blue = "BLUE",
    Red = 'RED',
    Green = 'GREEN'
}
console.log(Color.Blue);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

编译后:

// index.js
var Color;
(function (Color) {
    Color["Blue"] = "BLUE";
    Color["Red"] = "RED";
    Color["Green"] = "GREEN";
})(Color || (Color = {}));
console.log(Color.Blue);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用常量枚举:

// index.ts
const enum Color{
    Blue = "BLUE",
    Red = 'RED',
    Green = 'GREEN'
}
console.log(Color.Blue);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

编译后:

// index.js
console.log("BLUE" /* Blue */);
  • 1
  • 2

使用常量枚举可以提升编译的性能,不会将 ts 中的枚举编译成 JavaScrit 代码,而是直接使用常量值,这样就提高了性能。只有常量值才能使用常量枚举。

void

用于表示方法返回值的类型,表示该方法没有返回值,是一个返回值的占位符。

// 当方法没有返回值时,使用void作为占位符
function hello(): void {
    alert("Hello Runoob");
}
  • 1
  • 2
  • 3
  • 4

声明一个 void 类型的变量没有什么用,只能为变量赋值 undefinednull

let nothing: void = undefined
  • 1

null

表示对象值缺失。

JavaScriptnull 表示 “什么都没有”。null 是一个只有一个值的特殊类型。表示一个空对象引用。

typeof 检测 null 返回是 object

undefined

用于初始化变量为一个未定义的值。

JavaScript 中, undefined 是一个没有设置值的变量。typeof 一个没有值的变量会返回 undefined

NullUndefined 是其他任何类型(包括 void)的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 nullundefined。而在 TypeScript 中启用严格的空校验( --strictNullChecks )特性,就可以使得 nullundefined 只能被赋值给 void 或本身对应的类型,示例代码如下:

// 启用 --strictNullChecks
let x: number;
x = 1; // 运行正确
x = undefined;    // 运行错误
x = null;    // 运行错误
  • 1
  • 2
  • 3
  • 4
  • 5

上面的例子中变量 x 只能是数字类型。如果一个类型可能出现 nullundefined, 可以用 | 来支持多种类型,示例代码如下:

// 启用 --strictNullChecks
let x: number | null | undefined;
x = 1; // 运行正确
x = undefined;    // 运行正确
x = null;    // 运行正确
  • 1
  • 2
  • 3
  • 4
  • 5

never

never 是其他类型(包括 nullundefined)的子类型,代表从不会出现的值。这意味着声明为 never 类型的变量只能被 never 类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环),示例代码如下:

let x: never;
let y: number;

// 运行错误,数字类型不能转为 never 类型
x = 123;

// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error('exception')})();

// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error('exception')})();

// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
    throw new Error(message);
}

// 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
    while (true) {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

注意事项:

  1. TypeScript 中描述类型要用 小写,比如 booleannumberstring 等;
  2. 大写开头的如 BooleanNumberString 代表的是 JavaScript 的构造函数;
//通过 new Number('10') 得到的是一个构造函数,本质是一个对象
let a: Number = new Number('10') // a === 10 为 false

// Number('10') 与 10 都是声明一个数字 10 的方法,本质就是一个数字
let b: number = Number('10') // b === 10 为 true

/*
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。a 是一个对象,它的 __proto__ 属性指向该对象的构造函数的原型对象 Number,所以为 true。b 是一个数字,所以为 false。
*/
a instanceof Number // true
b instanceof Number // false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

__proto__ 是非标准属性,你也可以使用 Object.getPrototypeOf() 方法来访问一个对象的原型:

a.__proto__ === Object.getPrototypeOf(a) // true
  • 1

需要注意的是,TypeScript 一些基本类型,需要记住的是:TypeScript 中描述类型要用 小写。不要滥用 any

3.3、BigInt 类型

概念

bigint 是一种基本数据类型(primitive data type)。

JavaScript 中可以用 Number 表示的最大整数为 2^53 - 1,可以写为 Number.MAX_SAFE_INTEGER。如果超过了这个界限,可以用 BigInt来表示,它可以表示任意大的整数。

语法

在一个整数字面量后加 n 的方式定义一个 BigInt,如:10n 或者调用函数 BigInt()

const theBiggestInt: bigint = 9007199254740991n
const alsoHuge: bigint = BigInt(9007199254740991)
const hugeString: bigint = BigInt("9007199254740991")

theBiggestInt === alsoHuge // true
theBiggestInt === hugeString // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

BigIntNumber 的不同点:

  • BigInt 不能用于 Math 对象中的方法。
  • BigInt 不能和任何 Number 实例混合运算,两者必须转换成同一种类型。
  • BigInt 变量在转换为 Number 变量时可能会丢失精度。

使用 number 类型:

const biggest: number = Number.MAX_SAFE_INTEGER

//最大精度就是这个容器已经完全满了,无论往上加多少都会溢出,所以这两个值是相等的
const biggest1: number = biggest + 1
const biggest2: number = biggest + 2

biggest1 === biggest2 // true 超过精度
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

使用 BigInt 类型:

const biggest: bigint = BigInt(Number.MAX_SAFE_INTEGER)

//bigint 类型就是用来表示那些已经超出了 number 类型最大值的整数值,也就是这个容器还没满,在此基础上加上两个不同的值,其结果不相等
const biggest1: bigint = biggest + 1n
const biggest2: bigint = biggest + 2n

biggest1 === biggest2 // false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

类型信息

使用 typeof 检测类型时,BigInt 对象返回 bigint:

typeof 10n === 'bigint'         // true
typeof BigInt(10) === 'bigint'  // true

typeof 10 === 'number'          // true
typeof Number(10) === 'number'  // true
  • 1
  • 2
  • 3
  • 4
  • 5

运算

BigInt 可以正常使用 +-*/**% 符号进行运算:

const previousMaxSafe: bigint = BigInt(Number.MAX_SAFE_INTEGER)  // 9007199254740991n

const maxPlusOne: bigint = previousMaxSafe + 1n                  // 9007199254740992n

const multi: bigint = previousMaxSafe * 2n                       // 18014398509481982n

const subtr: bigint = multi – 10n                                // 18014398509481972n

const mod: bigint = multi % 10n                                  // 2n

const bigN: bigint = 2n ** 54n                                   // 18014398509481984n
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当使用 / 操作符时,会向下取整,不会返回小数部分:

const divided: bigint = 5n / 2n                                   // 2n, not 2.5n
  • 1

比较 与 条件

NumberBigInt 可以进行比较:

0n === 0 // false
0n == 0 // true
1n < 2  // true
2n > 1  // true
2 > 2   // false
2n > 2  // false
2n >= 2 // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

条件判断:

if (0n) {
  console.log('条件成立!');
} else {
  console.log('条件不成立!'); // 输出结果
}

0n || 10n    // 10n
0n && 10n    // 0n
Boolean(0n)  // false
Boolean(10n) // true
!10n         // false
!0n          // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.4、Symbol 类型

概念

symbol 是一种基本数据类型(primitive data type)。

Symbol() 函数会返回 symbol 类型的值。每个从 Symbol() 返回的 symbol 值都是唯一的。

语法

Symbol([description])

参数 description:可选的,字符串类型。

使用 Symbol() 创建新的 symbol 类型:

const sym1: symbol = Symbol()
const sym2: symbol = Symbol('foo')
const sym3: symbol = Symbol('foo')
  • 1
  • 2
  • 3

上面的代码创建了三个新的 symbol 类型,但要注意每个从 Symbol() 返回的值都是唯一的:

console.log(sym2 === sym3) // false
  • 1

每个 Symbol() 方法返回的值都是唯一的,所以,sym2sym3 不相等。

Symbol() 作为构造函数是不完整的:

const sym = new Symbol() // TypeError
  • 1

这种语法会报错,是因为从 ECMAScript 6 开始围绕原始数据类型创建一个显式包装器对象已不再被支持,但因历史遗留原因, new Boolean()new String() 以及 new Number() 仍可被创建:

const symbol = new Symbol()   // TypeError
const bigint = new BigInt()   // TypeError

const number = new Number()   // OK
const boolean = new Boolean() // OK
const string = new String()   // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

应用场景

1、当一个对象有较多属性时(往往分布在不同文件中由模块组合而成),很容易将某个属性名覆盖掉,使用 Symbol 值可以避免这一现象,比如 vue-router 中的 name 属性。

// a.js 文件
export const aRouter = {
  path: '/index',
  name: Symbol('index'),
  component: Index
},

// b.js 文件

export const bRouter = {
  path: '/home',
  name: Symbol('index'), // 不重复
  component: Home
},

// routes.js 文件
import { aRouter } from './a.js'
import { bRouter } from './b.js'

const routes = [
  aRouter,
  bRouter
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

两个不同文件使用了同样的 Symbol('index') 作为属性 name 的值,因 symbol 类型的唯一性,就避免了重复定义。

2、模拟类的私有方法。

const permission: symbol = Symbol('permission')

class Auth {
  [permission]() {
    // do something
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这种情况通过类的实例是无法取到该方法,模拟类的私有方法。

但是,TypeScript 是可以使用 private 关键字的,所以这种方法可以在 JavaScript 中使用。

3、判断是否可以用 for...of 迭代。

if (Symbol.iterator in iterable) {
    for(let n of iterable) {
      console.log(n)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • for...of 循环内部调用的是数据结构的 Symbol.iterator 方法。

  • for...of 只能迭代可枚举属性。

4、Symbol.prototype.description

Symbol([description]) 中可选的字符串即为这个 Symbol 的描述,如果想要获取这个描述:

const sym: symbol = Symbol('hello')

console.log(sym);               // Symbol(hello)
console.log(sym.toString());    // Symbol(hello)
console.log(sym.description);   // hello
  • 1
  • 2
  • 3
  • 4
  • 5

3.5、元组(Tuple

概念

相同类型元素组成成为数组,不同类型元素组成了元组(Tuple)。

语法

声明一个由 stringnumber 构成的元组:

const list: [string, number] = ['Sherlock', 1887]   // ok

const list1: [string, number] = [1887, 'Sherlock']  // error
  • 1
  • 2
  • 3

元组中规定的元素类型顺序必须是完全对照的,而且不能多、不能少,list1 中定义的第一个元素为 string类型,不能赋值为 number类型的数据。

当赋值或访问一个已知索引的元素时,会得到正确的类型:

const list: [string, number] = ['Sherlock', 1887]

//list[0] 是一个字符串类型,拥有 substr() 方法。
list[0].substr(1)  // ok

//list[1] 是一个数字类型,没有 substr() 方法,所以报错
list[1].substr(1)  // Property 'substr' does not exist on type 'number'.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

要注意元组的越界问题,虽然可以越界添加元素(不建议),但是不可越界访问:

const list: [string, number] = ['Sherlock', 1887]
//向一个声明了只有两个元素的元组继续添加元素,这种操作虽然可行,但是严重不建议!
list.push('hello world')

//该元组只有两个元素,不可越界访问第三个元素
console.log(list)      // ok [ 'Sherlock', 1887, 'hello world' ]
console.log(list[2])   // Tuple type '[string, number]' of length '2' has no element at index '2'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可选元素类型

元组类型允许在元素类型后缀一个 ? 来说明元素是可选的:

const list: [number, string?, boolean?]
list = [10, 'Sherlock', true]
list = [10, 'Sherlock']
list = [10]
  • 1
  • 2
  • 3
  • 4

可选元素必须在必选元素的后面,也就是如果一个元素后缀了 ? 号,其后的所有元素都要后缀 ? 号。

元组类型的 Rest 使用

元组可以作为参数传递给函数,函数的 Rest 形参可以定义为元组类型:

declare function rest(...args: [number, string, boolean]): void
  • 1

等价于:

declare function rest(arg1: number, arg2: string, arg3: boolean): void
  • 1

还可以这样:

const list: [number, ...string[]] = [10, 'a', 'b', 'c']

const list1: [string, ...number[]] = ['a', 1, 2, 3]
  • 1
  • 2
  • 3

Rest 元素指定了元组类型是无限扩展的,可能有零个或多个具有数组元素类型的额外元素。

关键字 declare 表示声明作用

3.6、枚举(Enum

语法

使用枚举我们可以定义一些带名字的常量。TypeScript 支持数字的和基于字符串的枚举。枚举类型弥补了 JavaScript 的设计不足,很多语言都拥有枚举类型。

当我们需要一组相同主题下的数据时,枚举类型就很有用了。

enum Direction { Up, Down, Left, Right }

enum Months { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }

enum Size { big = '大', medium = '中', small = '小' }

enum Agency { province = 1, city = 2, district = 3 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

代码中通过枚举类型分别声明了:不同的 方向,一年内不同的 月份,一个商品的不同 尺寸属性,经销商的不同 级别,这样的一组常量数据,是在一个相同主题下的不同表示。

数字枚举

声明一个枚举类型,如果没有赋值,它们的值默认为数字类型且从 0 开始累加:

enum Months {
  Jan,
  Feb,
  Mar,
  Apr
}

Months.Jan === 0 // true
Months.Feb === 1 // true
Months.Mar === 2 // true
Months.Apr === 3 // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

现实中月份是从 1 月开始的,那么只需要这样:

// 从第一个数字赋值,往后依次累加
enum Months {
  Jan = 1, //从属性 Jan 被赋值为 1 开始,后续的属性值依次累加
  Feb,
  Mar,
  Apr
}

Months.Jan === 1 // true
Months.Feb === 2 // true
Months.Mar === 3 // true
Months.Apr === 4 // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

字符串枚举

枚举类型的值为字符串类型

enum TokenType {
  ACCESS = 'accessToken',
  REFRESH = 'refreshToken'
}

// 两种不同的取值写法
console.log(TokenType.ACCESS === 'accessToken')        // true
console.log(TokenType['REFRESH'] === 'refreshToken')   // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

枚举的取值,有 TokenType.ACCESSTokenType['ACCESS'] 这两种不同的写法,效果是相同的。

数字类型和字符串类型可以混合使用,但是不建议:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}
  • 1
  • 2
  • 3
  • 4

计算常量成员

枚举类型的值可以是一个简单的计算表达式:

enum Calculate {
  a,
  b,
  expired = 60 * 60 * 24,
  length = 'hello'.length,
  plus = 'hello ' + 'world'
}

console.log(Calculate.expired)   // 86400
console.log(Calculate.length)    // 5
console.log(Calculate.plus)      // hello world
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 计算结果必须为常量。

  • 计算项必须放在最后。

反向映射

所谓的反向映射就是指枚举的取值,不但可以正向的 Months.Jan 这样取值,也可以反向的 Months[1] 这样取值。

enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

将上面的代码进行编译,查看编译后的 JavaScript 代码:

'use strict'
var Months;
(function (Months) {
  Months[Months['Jan'] = 1] = 'Jan'
  Months[Months['Feb'] = 2] = 'Feb'
  Months[Months['Mar'] = 3] = 'Mar'
  Months[Months['Apr'] = 4] = 'Apr'
})(Months || (Months = {}))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通过查看编译后的代码,可以得出:

console.log(Months.Mar === 3) // true

// 那么反过来能取到 Months[3] 的值吗?
console.log(Months[3])  // 'Mar'

// 所以
console.log(Months.Mar === 3)     // true
console.log(Months[3] === 'Mar')  // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 字符串枚举成员不会生成反向映射。

  • 枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。

const 枚举

在枚举上使用 const 修饰符:

enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}

const month = Months.Mar
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

查看一下编译后的内容:

'use strict'
const month = 3 /* Mar */
  • 1
  • 2

发现枚举类型应该编译出的对象没有了,只剩下 month 常量。这就是使用 const 关键字声明枚举的作用。因为变量 month 已经使用过枚举类型,在编译阶段 TypeScript 就将枚举类型抹去,这也是性能提升的一种方案。

枚举合并

分开声明名称相同的枚举类型,会自动合并:

enum Months {
  Jan = 1,
  Feb,
  Mar,
  Apr
}

enum Months {
  May = 5,
  Jun
}

console.log(Months.Apr) // 4
console.log(Months.Jun) // 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

编译后的 JavaScript 代码:

'use strict'
var Months;
(function (Months) {
  Months[Months['Jan'] = 1] = 'Jan'
  Months[Months['Feb'] = 2] = 'Feb'
  Months[Months['Mar'] = 3] = 'Mar'
  Months[Months['Apr'] = 4] = 'Apr'
})(Months || (Months = {}));
(function (Months) {
  Months[Months['May'] = 5] = 'May'
  Months[Months['Jun'] = 6] = 'Jun'
})(Months || (Months = {}))

console.log(Months.Apr) // 4
console.log(Months.Jun) // 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.7、Never 类型

概念

never 类型表示那些永不存在的值的类型。

never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never

应用场景

一个抛出异常的函数表达式,其函数返回值类型为 never

function error(message:string): never {
  throw new Error(message)
}
  • 1
  • 2
  • 3

同样的,不会有返回值的函数表达式,其函数返回值类型也为 never

// 推断的返回值类型为 never
function fail(): never {
    return error("Something failed")
}
  • 1
  • 2
  • 3
  • 4

不能取得值的地方:

interface Foo {
  type: 'foo'
}

interface Bar {
  type: 'bar'
}

type All = Foo | Bar

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      break
    case 'bar':
      break
    default:
      // 此处不能取值
      const exhaustiveCheck: never = val
      break
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.8、unknown 类型

any无需事先执行任何类型的检查:

let value: any

value = true             // OK
value = 10               // OK
value = "Hello World"    // OK
value = []               // OK
value = {}               // OK
value = Math.random      // OK
value = null             // OK
value = undefined        // OK
value = new TypeError()  // OK
value = Symbol('name')   // OK

value.foo.bar            // OK
value.trim()             // OK
value()                  // OK
new value()              // OK
value[0][1]              // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在许多情况下,这太宽松了。如果使用了 unknown 类型:

let value: unknown

value = true             // OK
value = 10               // OK
value = "Hello World"    // OK
value = []               // OK
value = {}               // OK
value = Math.random      // OK
value = null             // OK
value = undefined        // OK
value = new TypeError()  // OK
value = Symbol('name')   // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

所有对该 value 变量的分配都被认为是类型正确的。

可以尝试:

let value: unknown

let value1: unknown = value   // OK
let value2: any = value       // OK

let value3: boolean = value   // Error
let value4: number = value    // Error
let value5: string = value    // Error
let value6: object = value    // Error
let value7: any[] = value     // Error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到,该 unknown 类型只能分配给 any 类型和 unknown 类型本身。

继续尝试:

let value: unknown

value.foo.bar  // Error
value.trim()   // Error
value()        // Error
new value()    // Error
value[0][1]    // Error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

unknown 类型在被确定为某个类型之前,不能被进行诸如函数执行、实例化等操作,一定程度上对类型进行了保护。

在那些将取得任意值,但不知道具体类型的地方使用 unknown,而非 any

4、接口(Interface

4.1、接口的概念

接口是对 JavaScript 本身的随意性进行约束,通过定义一个接口,约定了变量、类、函数等应该按照什么样的格式进行声明,实现多人合作的一致性。TypeScript 编译器依赖接口用于类型检查,最终编译为 JavaScript 后,接口将会被移除。

// 语法格式
interface DemoInterface {

}
  • 1
  • 2
  • 3
  • 4

4.2、应用场景

在声明一个对象函数或者时,先定义接口,确保其数据结构的一致性。

在多人协作时,定义接口尤为重要。

4.3、接口的好处

过去我们写 JavaScript 定义一个函数:

function getClothesInfo(clothes) {
  console.log(clothes.price)
}

let myClothes = {
  color: 'black', 
  size: 'XL', 
  price: 98 
}
getClothesInfo(myClothes)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

之前我们写 JavaScript 这样是很正常的,但同时你可能会遇到下面这些问题:

getClothesInfo() // Uncaught TypeError: Cannot read property 'price' of undefined
getClothesInfo({ color: 'black' }) // undefined
  • 1
  • 2

相信原因你也知道,JavaScript弱类型 语言,并不会对传入的参数进行任何检测,错误在运行时才被发现。那么通过定义 接口,在编译阶段甚至开发阶段就避免掉这类错误,接口将检查类型是否和某种结构做匹配。

下面通过接口的方式重写之前的例子:

interface Clothes {
  color: string;
  size: string;
  price: number;
}

function getClothesInfo(clothes: Clothes) {
  console.log(clothes.price)
}

let myClothes: Clothes = { 
  color: 'black', 
  size: 'XL', 
  price: 98 
}

getClothesInfo(myClothes)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

代码中,定义了一个接口 Clothes,在传入的变量 clothes 中,它的类型为 Clothes。这样,就约束了这个传入对象的 外形 与接口定义一致。只要传入的对象满足上面的类型约束,那么它就是被允许的。

  • 定义接口要 首字母大写。
  • 只需要关注值的 外形,并不像其他语言一样,定义接口是为了实现。
  • 如果没有特殊声明,定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致。

4.4、接口的属性

可选属性

接口中的属性不全是必需的。可选属性的含义是该属性在被变量定义时可以不存在。

// 语法
interface Clothes {
  color?: string;
  size: string;
  price: number;
}

// 这里可以不定义属性 color
let myClothes: Clothes = { 
  size: 'XL', 
  price: 98 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。这时,仍不允许添加未定义的属性,如果引用了不存在的属性时 TS 将直接捕获错误。

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly 来指定只读属性,比如价格是不能被修改的:

// 语法
interface Clothes {
  color?: string;
  size: string;
  readonly price: number;
}

// 创建的时候给 price 赋值
let myClothes: Clothes = { 
  size: 'XL', 
  price: 98 
}

// 不可修改
myClothes.price = 100
// error TS2540: Cannot assign to 'price' because it is a constant or a read-only property
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

TypeScript 可以通过 ReadonlyArray<T> 设置数组为只读,那么它的所有写方法都会失效。

let arr: ReadonlyArray<number> = [1,2,3,4,5];
arr[0] = 6; // Index signature in type 'readonly number[]' only permits reading
  • 1
  • 2

最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。做为 变量 使用的话用 const,若做为 属性 则使用 readonly

任意属性

有时候我们希望接口允许有任意的属性,语法是用 [] 将属性包裹起来:

// 语法
interface Clothes {
  color?: string;
  size: string;
  readonly price: number;
  [propName: string]: any;
}

// 任意属性 activity
let myClothes: Clothes = { 
  size: 'XL', 
  price: 98,
  activity: 'coupon'
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里的接口 Clothes 可以有任意数量的属性,并且只要它们不是 color sizeprice,那么就无所谓它们的类型是什么。

案例:

使用 axios 库发起 HTTP 传输的时候,可以写入一个自定义的属性,就是因为源码中定义了一个任意属性:

this.$axios({
  method: 'put',
  url: '/cms/user',
  data: {
    nickname: this.nickname,
  },
  showBackend: true,
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4.5、函数类型

除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有 参数列表返回值类型 的函数定义。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean {
  return source.search(subString) > -1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。你可以改变函数的参数名,只要保证函数参数的位置不变。函数的参数会被逐个进行检查:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
// source => src, subString => sub
mySearch = function(src: string, sub: string): boolean {
  return src.search(sub) > -1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果你不想指定类型,TypeScript 的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc 类型变量。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(src, sub) {
  let result = src.search(sub);
  return result > -1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果接口中的函数类型带有函数名,下面两种书写方式是等价的:

interface Calculate {
  add(x: number, y: number): number
  multiply: (x: number, y: number) => number
}
  • 1
  • 2
  • 3
  • 4

4.6、可索引类型

代码示例:

// 正常的js代码
let arr = [1, 2, 3, 4, 5]
let obj = {
  brand: 'hello',
  type: 'education'
}

arr[0]
obj['brand']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

定义可索引类型接口:

interface ScenicInterface {
  [index: number]: string
}

let arr: ScenicInterface = ['西湖', '华山', '故宫']
let favorite: string = arr[0]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

示例中索引签名是 number类型,返回值是字符串类型。

另外还有一种索引签名是 字符串类型。我们可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。通过下面的例子理解这句话:

// 正确
interface Foo {
  [index: string]: number;
  x: number;
  y: number;
}

// 错误
interface Bar {
  [index: string]: number;
  x: number;
  y: string; // Error: y 属性必须为 number 类型
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

第 12 行,语法错误是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

4.7、类类型

我们希望类的实现必须遵循接口定义,那么可以使用 implements 关键字来确保兼容性。

这种类型的接口在传统面向对象语言中最为常见,比如 java 中接口就是这种类类型的接口。这种接口与抽象类比较相似,但是接口只能含有抽象方法和成员属性,实现类中必须实现接口中所有的抽象方法和成员属性。

interface AnimalInterface {
  name: string;
}

class Dog implements AnimalInterface {
  name: string;
  
  constructor(name: string){
    this.name = name
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

你也可以在接口中描述一个方法,在类里实现它:

interface AnimalInterface {
  name: string

  eat(m: number): string
}

class Dog implements AnimalInterface {
  name: string;

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

  eat(m: number) {
    return `${this.name}吃肉${m}分钟`
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

4.8、继承接口

和类一样,接口也可以通过关键字 extents 相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = {} as Square;
// 继承了 Shape 的属性
square.color = "blue";
square.sideLength = 10;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

一个接口可以继承多个接口,创建出多个接口的合成接口。

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4.9、混合类型

接口可以描述函数、对象的方法或者对象的属性。

有时希望一个对象同时具有上面提到多种类型,比如一个对象可以当做函数使用,同时又具有属性和方法。

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = function (start: number) { } as Counter;
  counter.interval = 123;
  counter.reset = function () { };
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

上面代码中,第 1 行,声明一个接口,如果只有 (start: number): string 一个成员,那么这个接口就是函数接口,同时还具有其他两个成员,可以用来描述对象的属性和方法,这样就构成了一个混合接口。

第 7 行,创建一个 getCounter() 函数,它的返回值是 Counter 类型的。

let counter = function (start: number) { } as Counter;
  • 1

第 8 行,通过类型断言,将函数对象转换为 Counter 类型,转换后的对象不但实现了函数接口的描述,使之成为一个函数,还具有 interval 属性和 reset() 方法。断言成功的条件是,两个数据类型只要有一方可以赋值给另一方,这里函数类型数据不能赋值给接口类型的变量,因为它不具有 interval 属性和 reset() 方法。

5、类(Class)

5.1、类的概念

类描述了所创建的对象共同的属性和方法。通过 class 关键字声明一个类,主要包含以下模块:

  • 属性
  • 构造函数
  • 方法

5.2、类的本质

JavaScript 中,生成实例对象可以通过构造函数的方式:

function Calculate (x, y) {
  this.x = x
  this.y = y
}

Calculate.prototype.add = function () {
  return this.x + this.y
}

var calculate = new Calculate(1, 2)
console.log(calculate.add()) // 3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

如果通过 class 关键字进行改写:

class Calculate {
  // 类的属性
  public x: number
  public y: number

  // 构造函数
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  // 类的方法
  add () {
    return this.x + this.y
  }
}

const calculate = new Calculate(1, 2)
console.log(calculate.add()) // 3

console.log(typeof Calculate) // 'function'
console.log(Calculate === Calculate.prototype.constructor) // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

最后一行,可以看出,类指向其构造函数本身,class 关键字可以看做是一个语法糖。

constructor() 方法是类的默认方法,通过 new 来生成对象实例时,自动调用该方法。换句话说,constructor() 方法默认返回实例对象 this

5.3、类的继承

基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类,这样可以抽出公共部分让子类复用。

使用 extends 关键字来实现继承:

// 继承 JavaScript 内置的 Date 对象
class LinDate extends Date {

  getFormattedDate() {
    var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    return this.getDate() + "-" + months[this.getMonth()] + "-" + this.getFullYear();
  }
}

const date = new LinDate()

console.log(date.getFullYear());     // 2020
console.log(date.getFormattedDate()) // 7-Jan-2020
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

LinDate 继承了 Date 的功能,可以使用父类 Date 的方法 getFullYear(),也可以使用自身的方法 getFormattedDate()

子类在 constructor 内中使用 super() 方法调用父类的构造函数,在一般方法内使用 super.method() 执行父类的方法。

class Animal {
  public name:string

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

  move(distance: number = 0) {
      console.log(`${this.name} moved ${distance}m.`)
  }
}

class Dog extends Animal {
  constructor(name: string) { 
    // 调用父类的构造函数
    super(name)
  }

  move(distance = 10) {
      console.log('bark...')
      // 执行父类的方法
      super.move(distance) 
  }
}

const dog: Animal = new Dog('Coco')

dog.move() // Coco moved 10m.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

5.4、访问修饰符

TypeScript 可以使用四种访问修饰符 publicprotectedprivatereadonly

public

TypeScript 中,类的成员全部默认为 public,当然你也可以显式的将一个成员标记为 public,标记为 public 后,在程序类的外部可以访问。

class Calculate {
  // 类的属性
  public x: number
  public y: number

  // 构造函数
  public constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  public add () {
    return this.x + this.y
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

protected

当成员被定义为 protected 后,只能被类的内部以及类的子类访问

class Base {
  //被定义成受保护的
  protected baseUrl: string = 'http://api.com/'

  constructor() {}

  protected request(method: string) {
    //类的内部可以访问受保护的成员属性
    const url = `${this.baseUrl}${method}`
    // TODO 封装基础的 http 请求
  }
}

class Address extends Base {
  get() {
    //子类可以访问父类中受保护的属性和方法
    return this.request('address')
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

private

当类的成员被定义为 private 后,只能被类的内部访问

class Mom {
  private labour() {
    return 'baby is coming'
  }
}

class Son extends Mom {
  test () {
    this.labour() // Error, Property 'labour' is private and only accessible within class 'Mom'
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在上面代码的第 9 行,父类中的 labour() 方法被定义为私有方法,只能在父类中被使用,子类中调用报错。

readonly

通过 readonly 关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化。

class Token {
  readonly secret: string = 'xjx*xh3GzW#3'

  readonly expired: number

  constructor (expired: number) {
    this.expired = expired
  } 
}

const token = new Token(60 * 60 * 24)
token.expired = 60 * 60 * 2 // Error, expired 是只读的
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上面代码中最后一行,因 Token 类的属性expired 被设置为只读属性,不可被修改。

5.5、静态方法

通过 static 关键字来创建类的静态成员,这些属性存在于类本身上面而不是类的实例上

class User {
  static getInformation () {
    return 'This guy is too lazy to write anything.'
  }
}

User.getInformation() // OK

const user = new User()
user.getInformation() // Error 实例中无此方法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

getInformation() 方法被定义为静态方法,只存在于类本身上,类的实例无法访问。

静态方法调用同一个类中的其他静态方法,可使用 this 关键字。

class StaticMethodCall {

  static staticMethod() {
      return 'Static method has been called'
  }
  static anotherStaticMethod() {
      return this.staticMethod() + ' from another static method'
  }
  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

静态方法中的 this 指向类本身,而静态方法也存在于类本身,所以可以在静态方法中用 this 访问在同一类中的其他静态方法。

非静态方法中,不能直接使用 this 关键字来访问静态方法。而要用类本身或者构造函数的属性来调用该方法:

class StaticMethodCall {
  constructor() {
      // 类本身调用
      console.log(StaticMethodCall.staticMethod())

      // 构造函数的属性调用
      console.log(this.constructor.staticMethod())
  }
  static staticMethod() {
      return 'static method has been called.'
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

类指向其构造函数本身,在非静态方法中,this.constructor === StaticMethodCalltrue, 也就是说这两种写法等价。

5.6、抽象类

抽象类作为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节。

abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

const animal = new Animal() // Error, 无法创建抽象类实例
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通常我们需要创建子类继承抽象类,将抽象类中的抽象方法一一实现,这样在大型项目中可以很好的约束子类的实现。

class Dog extends Animal {
  makeSound() {
    console.log('bark bark bark...')
  }
}

const dog = new Dog()

dog.makeSound()  // bark bark bark...
dog.move()       // roaming the earch...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

5.7、把类当做接口使用

类也可以作为接口来使用,这在项目中是很常见的。

class Pizza {
  constructor(public name: string, public toppings: string[]) {}
}

class PizzaMaker {
  // 把 Pizza 类当做接口
  static create(event: Pizza) {
    return new Pizza(event.name, event.toppings)
  }
}

const pizza = PizzaMaker.create({ 
  name: 'Cheese and nut pizza', 
  toppings: ['pasta', 'eggs', 'milk', 'cheese']
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

因为接口和类都定义了对象的结构,在某些情况下可以互换使用。如果你需要创建一个可以自定义参数的实例,同时也可以进行类型检查,把类当做接口使用不失为一个很好的方法。

这就是 TypeScript 的强大功能,而且非常灵活,拥有全面的面向对象设计和通用的类型检查

6、函数(Function)

6.1、函数的概念

JavaScript 中,函数是头等(first-class)对象,因为它们可以像任何其他对象一样具有属性和方法。在 JavaScript 中,每个函数都是一个 Function 对象。

TypeScript 又为 JavaScript 函数添加了一些额外的功能,让我们可以更容易地使用:

  • 函数类型
  • 可选参数
  • 默认参数
  • 剩余参数
  • 函数重载

6.2、函数类型

TypeScript 中编写函数,需要给形参和返回值指定类型:

const add = function(x: number, y: number): string {
  return (x + y).toString()
}
  • 1
  • 2
  • 3

参数 xy 都是 number 类型,两个参数相加后将其类型转换为 string, 所以整个函数的返回值为 string 类型。

上面的代码只是对 = 等号右侧的匿名函数进行了类型定义,等号左侧的 add 同样可以添加类型:

const add: (x: number, y: number) => string = function(x: number, y: number): string {
  return (x + y).toString()
}
  • 1
  • 2
  • 3

可以看到,等号左侧的类型定义由两部分组成:参数类型和返回值类型,通过 => 符号来连接。

这里要注意:函数类型的 => 和 箭头函数的 => 是不同的含义

通过箭头函数改写一下刚才写的函数:

const add = (x: number, y: number): string => (x + y).toString()
  • 1

等号左右两侧书写完整:

// 只要参数位置及类型不变,变量名称可以自己定义,比如把两个参数定位为 a b
const add: (a: number, b: number) => string = (x: number, y: number): string => (x + y).toString()
  • 1
  • 2

5.3、函数的参数

参数个数保持一致

TypeScript 中每个函数参数都是必须的。 这不是指不能传递 nullundefined 作为参数,而是说编译器会检查用户是否为每个参数都传入了值。简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

const fullName = (firstName: string, lastName: string): string => `${firstName}${lastName}`

let result1 = fullName('Sherlock', 'Holmes')
let result2 = fullName('Sherlock', 'Holmes', 'character') // Error, Expected 2 arguments, but got 3
let result3 = fullName('Sherlock')                        // Error, Expected 2 arguments, but got 1
  • 1
  • 2
  • 3
  • 4
  • 5

可选参数

JavaScript 中每个参数都是可选的,可传可不传。没传参的时候,它的值就是 undefined。 而在 TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能,可选参数必须跟在必须参数后面

const fullName = (firstName: string, lastName?: string): string => `${firstName}${lastName}`

let result1 = fullName('Sherlock', 'Holmes')
let result2 = fullName('Sherlock', 'Holmes', 'character') // Error, Expected 1-2 arguments, but got 3
let result3 = fullName('Sherlock')                        // OK
  • 1
  • 2
  • 3
  • 4
  • 5

默认参数

参数可以取默认值,上面介绍的可选参数必须跟在必须参数后面,而带默认值的参数不需要放在必须参数的后面,可随意调整位置

const token = (expired = 60*60, secret: string): void  => {}
// 或
const token1 = (secret: string, expired = 60*60 ): void => {}
  • 1
  • 2
  • 3

剩余参数

有的时候,函数的参数个数是不确定的,可能传入未知个数,这时没有关系,有一种方法可以解决这个问题。

通过 rest 参数 (形式为 ...变量名)来获取函数的剩余参数,这样就不需要使用 arguments 对象了。

function assert(ok: boolean, ...args: string[]): void {
  if (!ok) {
    throw new Error(args.join(' '));
  }
}

assert(false, '上传文件过大', '只能上传jpg格式')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意 rest 参数 只能是最后一个参数。

this参数

JavaScript 里,this 的值在函数被调用的时候才会被指定,但是这个 this 到底指的是什么还是需要花点时间弄清楚。

默认情况下,tsconfig.json 中,编译选项 compilerOptions 的属性 noImplicitThisfalse,我们在一个对象中使用的 this 时,它的类型是 any 类型。

let triangle = {
  a: 10,
  b: 15,
  c: 20,
  area: function () {
    return () => {
      // this 为 any 类型
      const p = (this.a + this.b + this.c) / 2
      return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
    }
  }
}

const myArea = triangle.area()
console.log(myArea())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在实际工作中 any 类型是非常危险的,我们可以添加任意属性到 any 类型的参数上,比如将 const p = (this.a + this.b + this.c) / 2 这句改为 const p = (this.d + this.d + this.d) / 2 也不会报错,这很容易造成不必要的问题。

所以我们应该明确 this 的指向,下面介绍两种方法:

第一种,在 tsconfig.json 中,将编译选项 compilerOptions 的属性 noImplicitThis 设置为 trueTypeScript 编译器就会帮你进行正确的类型推断:

let triangle = {
  a: 10,
  b: 15,
  c: 20,
  area: function () {
    return () => {
      const p = (this.a + this.b + this.c) / 2
      return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
    }
  }
}

const myArea = triangle.area()
console.log(myArea())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

noImplicitThis 设置为 true 以后,把鼠标放在第 7 行的 this 上,可以看到:

  this: {
    a: number;
    b: number;
    c: number;
    area: () => () => number;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这时,TypeScript 编译器就能准确的知道了 this 的类型,如果取不存在于 this 属性中的 d,将会报错 Property 'd' does not exist on type '{ a: number; b: number; c: number; area: () => () => any; }'

除了这种方法,我们还可以通过 this 参数 这种形式来解决 thisany 类型这一问题。提供一个显式的 this 参数,它出现在参数列表的最前面:

// 语法
function f(this: void) {

}
  • 1
  • 2
  • 3
  • 4

改造刚才的例子:

interface Triangle {
  a: number;
  b: number;
  c: number;
  area(this: Triangle): () => number;
}

let triangle: Triangle = {
  a: 10,
  b: 15,
  c: 20,
  area: function (this: Triangle) {
    return () => {
      const p = (this.a + this.b + this.c) / 2
      return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
    }
  }
}

const myArea = triangle.area()
console.log(myArea())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

我们声明了一个接口 Triangle,其中的函数类型显式的传入了 this 参数,这个参数的类型为 Triangle 类型(第 5 行):

area(this: Triangle): () => number;
  • 1

此时,在第 14 行,this 指向 Triangle,就可以进行正确的类型判断,如果取未定义参数,编译器将直接报错。

6.4、函数重载

函数重载是指函数根据传入不同的参数,返回不同类型的数据。

它的意义在于让你清晰的知道传入不同的参数得到不同的结果,如果传入的参数不同,但是得到相同类型的数据,那就不需要使用函数重载。

比如面试中常考的字符反转问题,这里就不考虑负数情况了,只是为了演示函数重载:

function reverse(target: string | number) {
  if (typeof target === 'string') {
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') {
    return +[...target.toString()].reverse().join('')
  }
}

console.log(reverse('hello'))   // olleh
console.log(reverse(23874800))  // 847832
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

编译器并不知道入参是什么类型的,返回值类型也不能确定。这时可以为同一个函数提供多个函数类型定义来进行函数重载。

(通过 --downlevelIteration 编译选项增加对生成器和迭代器协议的支持)

function reverse(x: string): string
function reverse(x: number): number

function reverse(target: string | number) {
  if (typeof target === 'string') {
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') {
    return +[...target.toString()].reverse().join('')
  }
}
console.log(reverse('hello'))   // olleh
console.log(reverse(23874800))  // 847832
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

因为这个反转函数在传入字符串类型的时候返回字符串类型,传入数字类型的时候返回数字类型,所以在前两行进行了两次函数类型定义。在函数执行时,根据传入的参数类型不同,进行不同的计算。

为了让编译器能够选择正确的检查类型,它会从重载列表的第一个开始匹配。因此,在定义重载时,一定要把最精确的定义放在最前面

6.5、使用函数时的注意事项

  1. 如果一个函数没有使用 return 语句,则它默认返回 undefined
  2. 调用函数时,传递给函数的值被称为函数的 实参(值传递),对应位置的函数参数被称为 形参
  3. 在函数执行时, this 关键字并不会指向正在运行的函数本身,而是 指向调用函数的对象
  4. arguments 对象是所有(非箭头)函数中都可用的 局部变量。你可以使用 arguments 对象在函数中引用函数的参数。

7、泛型(Generic)

7.1、泛型的概念

泛型在传统的面向对象语言中极为常见,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。

通俗来讲:泛型是指在定义函数、接口或者类时,未指定其参数类型,只有在运行时传入才能确定。那么此时的参数类型就是一个变量,通常用大写字母 T 来表示,当然你也可以使用其他字符,如:UK等。

语法:在函数名、接口名或者类名添加后缀 <T>

function generic<T>() {}
interface Generic<T> {}
class Generic<T> {}
  • 1
  • 2
  • 3

之所以使用泛型,是因为它帮助我们为不同类型的输入,复用相同的代码。

比如写一个最简单的函数,这个函数会返回任何传入它的值。如果传入的是 number 类型:

function identity(arg: number): number {
    return arg
}
  • 1
  • 2
  • 3

如果传入的是 string 类型:

function identity(arg: string): string {
    return arg
}
  • 1
  • 2
  • 3

通过泛型,可以把两个函数统一起来:

function identity<T>(arg: T): T {
  return arg
}
  • 1
  • 2
  • 3

需要注意的是,泛型函数的返回值类型是根据你的业务需求决定,并非一定要返回泛型类型 T:

function identity<T>(arg: T): string {
  return String(arg)
}
  • 1
  • 2
  • 3

入参的类型是未知的,但是通过 String 转换,返回字符串类型。

7.2、多个类型参数

泛型函数可以定义多个类型参数:

function extend<T, U>(first: T, second: U): T & U {
  for(const key in second) {
    (first as T & U)[key] = second[key] as any
  }
  return first as T & U
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这个函数用来合并两个对象,具体实现暂且不去管它,这里只需要关注泛型多个类型参数的使用方式,其语法为通过逗号分隔 <T, U, K>

7.3、泛型参数默认类型

函数参数可以定义默认值,泛型参数同样可以定义默认类型:

function min<T = number>(arr:T[]): T{
  let min = arr[0]
  arr.forEach((value)=>{
     if(value < min) {
         min = value
     }
  })
   return min
}
console.log(min([20, 6, 8n])) // 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

同样的不用去关注这个最小数函数的具体实现,要知道默认参数语法为 <T = 默认类型>

7.4、泛型类型与泛型接口

先看一下函数类型:

const add: (x: number, y: number) => string = function(x: number, y: number): string {
  return (x + y).toString()
}
  • 1
  • 2
  • 3

等号左侧的 (x: number, y: number) => string 为函数类型。

再看下泛型类型:

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: <T>(arg: T) => T = identity
  • 1
  • 2
  • 3
  • 4
  • 5

同样的等号左侧的 <T>(arg: T) => T 即为泛型类型,它还有另一种带有调用签名的对象字面量书写方式:{ <T>(arg: T): T }:

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: { <T>(arg: T): T } = identity
  • 1
  • 2
  • 3
  • 4
  • 5

这就引导我们去写第一个泛型接口了。把上面例子里的对象字面量拿出来作为一个接口:

interface GenericIdentityFn {
  <T>(arg: T): T
}

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

进一步,把泛型参数当作整个接口的一个参数,我们可以把泛型参数提前到接口名上。这样我们就能清楚的知道使用的具体是哪个泛型类型:

interface GenericIdentityFn<T> {
  (arg: T): T
}

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn<number> = identity
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意,在使用泛型接口时,需要传入一个类型参数来指定泛型类型。示例中传入了 number 类型,这就锁定了之后代码里使用的类型。

7.5、泛类型

使用泛型是因为可以复用不同类型的代码。下面用一个最小堆算法举例说明泛型类的使用:

class MinClass {
  public list: number[] = []
  add(num: number) {
    this.list.push(num)
  }
  min(): number {
    let minNum = this.list[0]
    for (let i = 0; i < this.list.length; i++) {
      if (minNum > this.list[i]) {
        minNum = this.list[i]
      }
    }
    return minNum
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

示例中我们实现了一个查找 number 类型的最小堆类,但我们的最小堆还需要支持字符串类型,此时就需要泛型的帮助了:

// 类名后加上 <T>
class MinClass<T> {
  public list: T[] = []
  add(num: T) {
    this.list.push(num)
  }
  min(): T {
    let minNum = this.list[0]
    for (let i = 0; i < this.list.length; i++) {
      if (minNum > this.list[i]) {
        minNum = this.list[i]
      }
    }
    return minNum
  }
}


let m = new MinClass<string>()
m.add('hello')
m.add('world')
m.add('generic')
console.log(m.min()) // generic
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

第 2 行,在声明 类 MinClass 的后面后加上了 <T>,这样就声明了泛型参数 T,作为一个变量可以是字符串类型,也可以是数字类型。

7.6、泛型约束

语法:通过 extends 关键字来实现泛型约束。

如果我们很明确传入的泛型参数是什么类型,或者明确想要操作的某类型的值具有什么属性,那么就需要对泛型进行约束。通过两个例子来说明:

interface User {
  username: string
}

function info<T extends User>(user: T): string {
  return 'hello ' + user.username
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

示例中,第 5 行,我们约束了入参 user 必须包含 username 属性,否则在编译阶段就会报错。

下面再看另外一个例子:

type Args = number | string

class MinClass<T extends Args> {}

const m = new MinClass<boolean>() // Error, 必须是 number | string 类型
  • 1
  • 2
  • 3
  • 4
  • 5

在上面代码中,第 3 行,约束了泛型参数 T 继承自类型 Args,而类型 Args 是一个由 numberstring 组成的联合类型。

第 5 行,泛型参数只能是 numberstring 中的一种,传入 boolean 类型是错误的。

7.7、多重类型泛型约束

通过 <T extends Interface1 & Interface2> 这种语法来实现多重类型的泛型约束:

interface Sentence {
  title: string,
  content: string
}

interface Music {
  url: string
}

class Classic<T extends Sentence & Music> {
  private prop: T

  constructor(arg: T) {
    this.prop = arg
  }

  info() {
    return {
      url: this.prop.url,
      title: this.prop.title,
      content: this.prop.content
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在代码中,第 10 行,约束了泛型参数 T 需继承自交叉类型(后续有单节介绍) Sentence & Music,这样就能访问两个接口类型的参数。

8、TypeScript 类型进阶

8.1、字面量类型

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。通俗的讲,字面量也可以叫直接量,就是你看见什么,它就是什么。

字符串类型,其实是一个集合类型,所有的字符串集合在一起构成了 string 类型。而字符串字面量类型就直接多了,你定义为 'hello',那这个变量的类型就是 'hello' 类型。

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。

let protagonist: 'Sherlock'

protagonist = 'Sherlock'
protagonist = 'Watson' // Error, Type '"Watson"' is not assignable to type '"Sherlock"'
  • 1
  • 2
  • 3
  • 4

变量 protagonist 被声明为 'Sherlock' 字面量类型,就只能赋值为 'Sherlock'

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'

class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
      if (easing === 'ease-in') {}
      else if (easing === 'ease-out') {}
      else if (easing === 'ease-in-out') {}
      else {
          // Error, 不应该传递 null 或 undefined
      }
  }
}

let button = new UIElement()
button.animate(0, 0, 'ease-in')
button.animate(0, 0, 'uneasy') // Error, 'uneasy' 不被允许
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在上面代码中,第 1 行,通过类型别名,声明了类型 Easing'ease-in' | 'ease-out' | 'ease-in-out'
这样三个字符串字面量构成的联合类型。

第 4 行,你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: 'img'): HTMLImageElement
function createElement(tagName: 'input'): HTMLInputElement

function createElement(tagName: string): Element {}
  • 1
  • 2
  • 3
  • 4

如果参数 tagName'img' 类型,返回值类型为 HTMLImageElement; 如果参数 tagName'input' 类型,返回值类型为 HTMLInputElement

布尔字面量类型

声明布尔字面量类型,注意这里是 : 不是 == 等号是变量赋值,: 表示声明的类型。

let success: true
let fail: false
let value: true | false
  • 1
  • 2
  • 3

接口的返回值,会有正确返回和异常两种情况,这两种情况要有不同的数据返回格式:

type Result = { success: true, code: number, object: object } | { success: false, code: number, errMsg: string }

let res: Result = { success: false, code: 90001, errMsg: '该二维码已使用' }

if (!res.success) {
  res.errMsg // OK
  res.object // Error, Property 'object' does not exist on type '{ success: false; code: number; errMsg: string; }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

类型别名 Result 是一个由两个对象组成的联合类型,都有一个共同的 success 属性,这个属性的类型就是布尔字面量类型。

数字字面量类型

TypeScript 还具有数字字面量类型。

比如骰子只有六种点数:

let die: 1 | 2 | 3 | 4 | 5 | 6

die = 9 // Error
  • 1
  • 2
  • 3

8.2、类型推断

类型推断的含义是不需要指定变量类型或函数的返回值类型,TypeScript 可以根据一些简单的规则推断其的类型。

基础类型推断

基础的类型推断发生在 初始化变量,设置默认参数和决定返回值时。

初始化变量例子:

let x = 3             // let x: number
let y = 'hello world' // let y: string

let z                 // let z: any
  • 1
  • 2
  • 3
  • 4

变量 x 的类型被推断为数字,变量 y 的类型被推断为字符串。如果定义时没有赋值,将被推断为 any 类型。

设置默认参数和决定返回值时的例子:

// 返回值推断为 number
function add(a:number, b:10) {
  return a + b
}

const obj = {
  a: 10,
  b: 'hello world'
}

obj.b = 15 // Error,Type '15' is not assignable to type 'string'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在上面代码中,第 1 行,参数 b 有默认值 10,被推断为 number 类型。

第 2 行,两个 number 类型相加,函数 add() 返回值被推断为 number 类型。

最后一行,obj 的类型被推断为 {a: number, b: string},所以属性 b 不能被赋值为数字。

const obj = {
  protagonist: 'Sherlock',
  gender: 'male'
}

let { protagonist } = obj
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最佳通用类型推断

当需要从多个元素类型推断出一个类型时,TypeScript 会尽可能推断出一个兼容所有类型的通用类型。

比如声明一个数组:

let x = [1, 'hello', null]
  • 1

为了推断 x 的类型,必须考虑所有的元素类型。这里有三种元素类型 numberstringnull,此时数组被推断为 let x: (string | number | null)[] 联合类型。

是否兼容 null 类型可以通过 tsconfig.json 文件中属性 strictNullChecks 的值设置为 truefalse 来决定。

上下文类型推断

前面两种都是根据从右向左流动进行类型推断,上下文类型推断则是从左向右的类型推断。

例如定义一个 Animal 的类作为接口使用:

class Animal {
  public species: string | undefined
  public weight: number | undefined
}

const simba: Animal = {
  species: 'lion',
  speak: true  // Error, 'speak' does not exist in type 'Animal'
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在上面代码中,第 6 行,将 Animal 类型显示的赋值给 变量 simbaAnimal 类型 没有 speak 属性,所以不可赋值。

8.3、类型断言

TypeScript 允许你覆盖它的推断,毕竟作为开发者你比编译器更了解你写的代码。

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

关键字 as

当你把 JavaScript 代码迁移到 TypeScript 时,一个常见的问题:

const user = {}

user.nickname = 'Evan'  // Error, Property 'nickname' does not exist on type '{}'
user.admin = true       // Error, Property 'admin' does not exist on type '{}'
  • 1
  • 2
  • 3
  • 4

编译器推断 const user: {},这是一个没有属性的对象,所以你不能对其添加属性。

此时可以使用类型断言(as关键字)覆盖其类型推断:

interface User {
  nickname: string,
  admin: boolean,
  groups: number[]
}

const user = {} as User

user.nickname = 'Evan' 
user.admin = true       
user.groups = [2, 6]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在上面代码中,第 7 行,这里通过 as 关键字进行类型断言,将变量 user 的类型覆盖为 User 类型。但是请注意,类型断言不要滥用,除非你完全明白你在干什么。

首尾标签

类型断言还可以通过标签 <> 来实现:

interface User {
  nickname: string,
  admin: boolean,
  groups: number[]
}

const user = <User>{} // User类型

user.nickname = 'Evan' 
user.admin = true       
user.groups = [2, 6]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在上面代码中,第 7 行,使用 <User>{} 这种标签形式,将变量 user 强制断言为 User 类型。

但是,当你在使用 JSX 语法时,会跟标签 <> 形式的类型断言混淆:

let nickname = <User>Evan</User>  // 这里的 User 指向一个 component
  • 1

所以,建议统一使用 as type 这种语法来为类型断言。

非空断言

如果编译器不能够去除 nullundefined,可以使用非空断言 ! 手动去除。

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // name 被断言为非空
  }
  name = name || "Bob"
  return postfix("great")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在上面代码中,第 2 行,postfix() 是一个嵌套函数,因为编译器无法去除嵌套函数的 null (除非是立即调用的函数表达式),所以 TypeScript 推断第 3 行的 name 可能为空。

第 5 行,而 name = name || "Bob" 这行代码已经明确了 name 不为空,所以可以直接给 name 断言为非空(第 3 行)。

双重断言

双重断言极少有应用场景,只需要知道有这种操作即可:

interface User {
  nickname: string,
  admin: boolean,
  group: number[]
}

const user = 'Evan' as any as User
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在上面代码中,最后一行,使用 as 关键字进行了两次断言,最终变量 user 被强制转化为 User 类型。

8.4、类型保护

类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作。

typeof

通过 typeof 运算符判断变量类型,下面看一个之前介绍函数重载时的例子:

function reverse(target: string | number) {
  if (typeof target === 'string') {
    target.toFixed(2) // Error,在这个代码块中,target 是 string 类型,没有 toFixed 方法
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') {
    target.toFixed(2) // OK
    return +[...target.toString()].reverse().join('')
  }

  target.forEach(element => {}) // Error,在这个代码块中,target 是 string 或 number 类型,没有 forEach 方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在上面代码中,第 2 行,通过 typeof 关键字,将这个代码块中变量 target 的类型限定为 string 类型。

第 6 行,通过 typeof 关键字,将这个代码块中变量 target 的类型限定为 number 类型。

第 11 行,因没有限定,在这个代码块中,变量 targetstringnumber 类型,没有 forEach 方法,所以报错。

instanceof

instanceoftypeof 类似,区别在于 typeof 判断基础类型,instanceof 判断是否为某个对象的实例:

class User {
  public nickname: string | undefined
  public group: number | undefined
}

class Log {
  public count: number = 10
  public keyword: string | undefined
}

function typeGuard(arg: User | Log) {
  if (arg instanceof User) {
    arg.count = 15 // Error, User 类型无此属性
  }

  if (arg instanceof Log) {
    arg.count = 15 // OK
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在上面代码中,第 12 行,通过 instanceof 关键字,将这个代码块中变量 arg 的类型限定为 User 类型。

第 16 行,通过 instanceof 关键字,将这个代码块中变量 arg 的类型限定为 Log 类型。

in

in 操作符用于确定属性是否存在于某个对象上,这也是一种缩小范围的类型保护。

class User {
  public nickname: string | undefined
  public groups!: number[]
}

class Log {
  public count: number = 10
  public keyword: string | undefined
}

function typeGuard(arg: User | Log) {
  if ('nickname' in arg) {
    // (parameter) arg: User,编辑器将推断在当前块作用域 arg 为 User 类型
    arg.nickname = 'hello'
  }

  if ('count' in arg) {
    // (parameter) arg: Log,编辑器将推断在当前块作用域 arg 为 Log 类型
    arg.count = 15
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

上面代码中,第 12 行,通过 in 关键字,将这个代码块中变量 arg 的类型限定为 User 类型。

第 17 行,通过 in 关键字,将这个代码块中变量 arg 的类型限定为 Log 类型。

字面量类型保护

用字面量类型那一节的例子改造一下来介绍字面量类型保护:

type Success = {
  success: true,
  code: number,
  object: object
}

type Fail = {
  success: false,
  code: number,
  errMsg: string,
  request: string
}

function test(arg: Success | Fail) {
  if (arg.success === true) {
    console.log(arg.object) // OK
    console.log(arg.errMsg) // Error, Property 'errMsg' does not exist on type 'Success'
  } else {
    console.log(arg.errMsg) // OK
    console.log(arg.object) // Error, Property 'object' does not exist on type 'Fail'
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

上面代码中,第 15 行,通过布尔字面量,将这个代码块中变量 arg 的类型限定为 Success 类型。

第 18 行,通过布尔字面量,将这个代码块中变量 arg 的类型限定为 Fail 类型。

8.5、类型兼容性

类型兼容性用于确定一个类型是否能赋值给其他类型。

TypeScript 的类型检查机制都是为了让开发者在编译阶段就可以直观的发现代码书写问题,养成良好的代码规范从而避免很多低级错误。

let address: string = 'Baker Street 221B'
let year: number = 2010
address = year // Error
  • 1
  • 2
  • 3

在上面代码中, 第 3 行,类型 ‘number’ 不能赋值给类型 ‘string’

结构化

TypeScript 类型兼容性是基于结构类型的;结构类型只使用其成员来描述类型。

TypeScript 结构化类型系统的基本规则是,如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性。比如:

interface User {
  name: string,
  year: number
}

let protagonist = {
  name: 'Sherlock·Holmes',
  year: 1854,
  address: 'Baker Street 221B'
}

let user: User = protagonist // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

接口 User 中的每一个属性在 protagonist 对象中都能找到对应的属性,且类型匹配。另外,可以看到 protagonist 具有一个额外的属性 address,但是赋值同样会成功。

比较两个函数

相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,难的是如何判断两个函数是否兼容。判断两个函数是否兼容,首先要看参数是否兼容,第二个还要看返回值是否兼容。

  • 函数参数

代码示例:

let fn1 = (a: number, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}

fn2 = fn1 // OK
fn1 = fn2 // Error
  • 1
  • 2
  • 3
  • 4
  • 5

上面代码中,第 4 行,将 fn1 赋值给 fn2 成立是因为:

  1. fn1 的每个参数均能在 fn2 中找到对应类型的参数
  2. 参数顺序保持一致,参数类型对应
  3. 参数名称不需要相同

第 5 行,将 fn2 赋值给 fn1 不成立,是因为 fn2 中的必须参数必须在 fn1 中找到对应的参数,显然第三个布尔类型的参数在 fn1 中未找到。

参数类型对应即可,不需要完全相同:

let fn1 = (a: number | string, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}

fn2 = fn1 // OK
  • 1
  • 2
  • 3
  • 4

fn1 的第一个参数是 numberstring 的联合类型,可以对应 fn2 的第一个参数类型 number,所以第 4 行赋值正常。

  • 函数返回值

创建两个仅是返回值类型不同的函数:

let x = () => ({name: 'Alice'})
let y = () => ({name: 'Alice', location: 'Seattle'})

x = y // OK
y = x // Error
  • 1
  • 2
  • 3
  • 4
  • 5

在上面代码中,最后一行,函数 x() 缺少 location 属性,所以报错。

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。由此可以得出如果目标函数的返回值类型是 void,那么源函数返回值可以是任意类型:

let x : () => void
let y = () => 'hello'

x = y // OK
  • 1
  • 2
  • 3
  • 4

枚举的类型兼容性

枚举与数字类型相互兼容:

enum Status {
  Pending,
  Resolved,
  Rejected
}

let current = Status.Pending
let num = 0

current = num
num = current
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

不同枚举类型之间是不兼容的:

enum Status { Pending, Resolved, Rejected }
enum Color { Red, Blue, Green }

let current = Status.Pending
current = Color.Red // Error
  • 1
  • 2
  • 3
  • 4
  • 5

类的类型兼容性

类与对象字面量和接口的兼容性非常类似,但是类分实例部分和静态部分。

比较两个类类型数据时,只有实例成员会被比较,静态成员和构造函数不会比较。

class Animal {
  feet!: number
  constructor(name: string, numFeet: number) { }
}

class Size {
  feet!: number
  constructor(numFeet: number) { }
}

let a: Animal
let s: Size

a = s!  // OK
s = a  // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

类 Animal 和类 Size 有相同的实例成员 feat 属性,且类型相同,构造函数参数虽然不同,但构造函数不参与两个类类型比较,所以最后两行可以相互赋值。

类的私有成员和受保护成员会影响兼容性。允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

class Animal {
  protected feet!: number
  constructor(name: string, numFeet: number) { }
}

class Dog extends Animal {}

let a: Animal
let d: Dog

a = d! // OK
d = a // OK

class Size {
  feet!: number
  constructor(numFeet: number) { }
}

let s: Size

a = s! // Error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在上面代码中,第 13 行,子类可以赋值给父类。

第 14 行,父类之所以能够给赋值给子类,是因为子类中没有成员。

最后一行,因为类 Animal 中的成员 feet 是受保护的,所以不能赋值成功。

泛型的类型兼容性

泛型的类型兼容性根据其是否被成员使用而不同。先看一段代码示例:

interface Empty<T> {}

let x: Empty<number>
let y: Empty<string>

x = y! // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上面代码里,x 和 y 是兼容的,因为它们的结构使用类型参数时并没有什么不同。但是当泛型被成员使用时:

interface NotEmpty<T> {
  data: T
}
let x: NotEmpty<number>
let y: NotEmpty<string>

x = y! // Error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在上面代码中, 因为第 4 行,泛型参数是 number 类型,第 5 行,泛型参数是 string 类型,所以最后一行赋值失败。

如果没有指定泛型类型的泛型参数,会把所有泛型参数当成 any 类型比较。

let identity = function<T>(x: T): void {
  // ...
}

let reverse = function<U>(y: U): void {
  // ...
}

identity = reverse // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

8.6、交叉类型

交叉类型是将多个类型合并为一个类型。

这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

语法为:类型一 & 类型二

示例代码:

interface Admin {
  id: number,
  administrator: string,
  timestamp: string
}

interface User {
  id: number,
  groups: number[],
  createLog: (id: number) => void,
  timestamp: number
}

let t: Admin & User

t!.administrator // 合法 Admin.administrator: string
t!.groups        // 合法 User.groups: number[]
t!.id            // 合法 id: number
t!.timestamp     // 合法 timestamp: never
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

交叉类型 Admin & User 包含了原类型的所有属性,但是要注意两个接口都拥有 idtimestamp 属性,且 id 类型相同,timestamp 类型不同。在此交叉类型中,timestamp 属性类型冲突,不可被赋值。

应用场景

我们大多是在混入(Mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。下面是合并两传入对象的成员属性的例子:

function extend<T, U>(first: T, second: U): T & U {
  for(const key in second) {
    (first as T & U)[key] = second[key] as any
  }
  return first as T & U
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数返回结果的类型是两个对象的交叉类型。调用 extend 函数,实现两个对象的合并:

class Person {
  constructor(public name: string) { }
}
class ConsoleLogger {
  log() {}
}

let jim = extend(new Person('Jim'), new ConsoleLogger())
let n = jim.name
jim.log()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

通过 extend() 函数合并了两个类的实例,我们知道交叉类型是 and 的意思,那么合并后即可访问 Person 类实例的 name 属性,也可以调用 ConsoleLogger 类实例的 log() 方法。

8.7、联合类型

联合类型与交叉类型很有关联,但是使用上却完全不同。区别在于:联合类型表示取值为多种中的一种类型,而交叉类型每次都是多个类型的合并类型。

语法为:类型一 | 类型二

联合类型之间使用竖线 “|” 分隔:

let currentMonth: string | number

currentMonth = 'February'
currentMonth = 2
  • 1
  • 2
  • 3
  • 4

在上面代码中, 第 1 行,表示 currentMonth 的值可以是 string 类型或者 number 类型中的一种。

联合类型的构成元素除了类型,还可以是字面量:

type Scanned = true | false
type Result = { status: 200, data: object } | { status: 500, request: string}
  • 1
  • 2

在上面代码中,第 1 行,表示类型别名 Scanned 可以是 true 或者 false 两种布尔字面量中的一种。

第 2 行,表示类型别名 Result 可以是 { status: 200, data: object } 或者 { status: 500, request: string} 两个对象字面量中的一种。

访问联合类型成员

如果一个值是联合类型,那么只能访问联合类型的共有属性或方法。

interface Dog {
  name: string,
  eat: () => void,
  destroy: () => void
}

interface Cat {
  name: string,
  eat: () => void,
  climb: () => void
}

let pet: Dog | Cat
pet!.name    // OK
pet!.eat()   // OK
pet!.climb() // Error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在上面代码中,第 13 行,声明变量 petDog | Cat 联合类型,那么变量 pet 可以访问接口 Dog 和 接口 Cat 共有的 name 属性和 eat() 方法。访问接口 Cat 独有的 climb() 方法是错误的。

可辨识联合

求不同图形面积的综合性实例:

interface Rectangle {
  type: 'rectangle',
  width: number,
  height: number
}

interface Circle {
  type: 'circle',
  radius: number
}

interface Parallelogram {
  type: 'parallelogram',
  bottom: number,
  height: number
}

function area(shape: Rectangle | Circle | Parallelogram) {
  switch (shape.type) {
    case 'rectangle':
      return shape.width * shape.height
    case 'circle':
      return Math.PI * Math.pow(shape.radius, 2)
    case 'parallelogram':
      return shape.bottom * shape.height
  }
}

let shape: Circle = {
  type: 'circle',
  radius: 10
}

console.log(area(shape))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

在上面代码中,第 18 行,函数 area() 的参数是一个 Rectangle | Circle | Parallelogram 联合类型。

其中,每个接口都有一个 type 属性,根据其不同的字符串字面量类型引导到不同的 case 分支,这种情况我们称之为可辨识联合(Discriminated Union)。

8.8、类型别名

类型别名会给类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

用关键字 type 定义类型别名。

类型别名不会新建一个类型,而是创建一个新名字来引用此类型。

先看下面几个例子,

原始类型:

type brand = string
type used = true | false

const str: brand = 'hello'
const state: used = true
  • 1
  • 2
  • 3
  • 4
  • 5

联合类型:

type month = string | number

const currentMonth: month = 'February'
const nextMonth: month = 3
  • 1
  • 2
  • 3
  • 4

交叉类型:

interface Admin {
  id: number,
  administrator: string,
  timestamp: string
}

interface User {
  id: number,
  groups: number[],
  createLog: (id: number) => void,
  timestamp: number
}

type T = Admin & User
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

同接口一样,类型别名也可以是泛型:

type Tree<T, U> = {
  left: T,
  right: U
}
  • 1
  • 2
  • 3
  • 4

接口 vs 类型别名

类型别名看起来和接口非常类似,区别之处在于:

  • 接口可以实现 extendsimplements,类型别名不行。
  • 类型别名并不会创建新类型,是对原有类型的引用,而接口会定义一个新类型。
  • 接口只能用于定义对象类型,而类型别名的声明方式除了对象之外还可以定义交叉、联合、原始类型等。

类型别名是最初 TypeScript 做类型约束的主要形式,后来引入接口之后,TypeScript 推荐我们尽可能的使用接口来规范我们的代码。

8.9、索引类型

下面是一个常见的JavaScript 函数,实现从一个对象中选取指定属性,得到它们的属性值:

function pluck(o, names) {
  return names.map(n => o[n])
}
  • 1
  • 2
  • 3

实现这样一个函数的类型定义要满足:

  • 数组参数 names 中的元素,只能是对象 o 身上有的属性。
  • 返回类型取决于参数 o 身上属性值的类型。

我们可以通过索引类型实现这样的类型定义。

索引类型可以让 TypeScript 编译器覆盖检测到使用了动态属性名的代码。

要理解抽象的索引类型,需要先理解索引类型查询操作符(keyof)和索引访问操作符(T[K])。

索引类型查询操作符 - keyof

keyof 可以获取对象的可访问索引字符串字面量类型。

interface User {
  id: number,
  phone: string,
  nickname: string,
  readonly department: string,
}

class Token{
  private secret: string | undefined
  public accessExp: number = 60 * 60
  public refreshExp: number = 60 * 60 * 24 * 30 * 3
}

let user: keyof User // let user: "id" | "phone" | "nickname" | "department"
type token = keyof Token // type token = "accessExp" | "refreshExp"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在上面代码中,倒数第二行,通过 let user: keyof User 得到了等价的 let user: "id" | "phone" | "nickname" | "department"

最后一行,通过 type token = keyof Token 得到了等价的 type token = "accessExp" | "refreshExp",注意这里没有 secret

可以看到对于任何类型 Tkeyof T 的结果为 T 上已知的公共属性名的联合。

索引访问操作符 - T[K]

通过 keyof 拿到了属性名,接下来还要拿到属性名对应属性值的类型。

还是以上面的 Token 类为例:

class Token{
  public secret: string = 'ixeFoe3x.2doa'
  public accessExp: number = 60 * 60
  public refreshExp: number = 60 * 60 * 24 * 30 * 3
}

type token = keyof Token
type valueType = Token[token] // type valueType = string | number
type secret = Token['secret'] // type secret = string
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

通过 Token['secret'] 拿到了属性 secret 的类型为 string

那么这时,我们知道了一个对象的类型为泛型 T,这个对象的属性类型 K 只需要满足 K extends keyof T,即可得到这个属性值的类型为 T[K]

理解了上面这段话,即可定义下面这个函数:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
  return o[name]; // o[name] is of type T[K]
}
  • 1
  • 2
  • 3

已知参数 o 的类型为 T,参数 name 的类型 K 满足 K extends keyof T,那么返回值的类型即为 T[K]

函数 pluck() 的类型定义

掌握了 keyofT[K],下面来完整的书写前言中的函数 pluck()

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n])
}

interface Person {
  name: string
  position: string
  age: number
}
let person: Person = {
  name: 'Evan',
  position: 'Software Engineer',
  age: 27
}

let values: unknown[] = pluck(person, ['name', 'age'])
console.log(values)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

参数 names: K[] 这种写法表示数组类型。

8.10、映射类型

映射类型可以将已知类型的每个属性都变为可选的或者只读的。

ReadonlyPartial 关键字

先来看这样一个任务:将 Person 接口的每个属性都变为可选属性或者只读属性。

interface Person{
  name: string
  age: number
}

type PersonOptional = Partial<Person>
type PersonReadonly = Readonly<Person>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在上面代码中,第 6 行,通过 Partial<Person> 这样的语法格式得到类型别名 PersonOptional,等价于:

type PersonOptional = {
  name?: string
  age?: number
}
  • 1
  • 2
  • 3
  • 4

第 7 行,通过 Readonly<Person> 这样的语法格式得到类型别名 PersonReadonly,等价于:

type PersonReadonly = {
  readonly name: string
  readonly age: number
}
  • 1
  • 2
  • 3
  • 4

两个关键字的源码分析

来看它们的实现源码:

type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}
type Partial<T> = {
  [K in keyof T]?: T[K]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

源码就使用了映射类型的语法 [K in Keys],来看这个语法的两个部分:

  1. 类型变量 K:它会依次绑定到每个属性,对应每个属性名的类型。
  2. 字符串字面量构成的联合类型的 Keys:它包含了要迭代的属性名的集合。

我们可以使用 for...in 来理解,它可以遍历目标对象的属性。

接下来继续分析:

  • Keys,可以通过 keyof 关键字取得,假设传入的类型是泛型 T,得到 keyof T,即为字符串字面量构成的联合类型("name" | "age")。
  • [K in keyof T],将属性名一一映射出来。
  • T[K],得到属性值的类型。

已知了这些信息,我们就得到了将一个对象所有属性变为可选属性的方法:

[K in keyof T]?: T[K]
  • 1

进而可得:

type Partial<T> = {
  [K in keyof T]?: T[K]
}
  • 1
  • 2
  • 3

Readonly<T>Partial<T> 都有着广泛的用途,因此它们与 Pick 一同被包含进了 TypeScript 的标准库里:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

interface User {
  id: number
  age: number
  name: string
}

type PickUser = Pick<User, 'id'>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在上面代码中,最后一行,就相当于 type PickUser = { id: number }

8.11、条件类型

条件类型用来表达非均匀类型映射,可以根据一个条件表达式来进行类型检测,从两个类型中选出其中一个:

T extends U ? X : Y
  • 1

语义类似三目运算符:若 TU 的子类型,则类型为 X,否则类型为 Y。若无法确定 T 是否为 U 的子类型,则类型为 X | Y

代码示例:

declare function f<T extends boolean>(x: T): T extends true ? string : number

const x = f(Math.random() < 0.5) // const x: string | number

const y = f(true) // const y: string
const z = f(false) // const z: number
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在上面代码中,第 3 行,可以看到在条件不确定的情况下,得到了联合类型 string | number

最后两行,条件确定时,得到了具体类型 stringnumber

可分配条件类型

在条件类型 T extends U ? X : Y 中,当泛型参数 T 取值为 A | B | C 时,这个条件类型就等价于 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y),这就是可分配条件类型。

可分配条件类型(distributive conditional type)中被检查的类型必须是裸类型参数(naked type parameter)。裸类型表示没有被包裹(Wrapped) 的类型,(如:Array<T>[T]Promise<T> 等都不是裸类型),简而言之裸类型就是未经过任何其他类型修饰或包装的类型。

应用场景

有了这些前置知识,我们就可以分析一下 TypeScript 内置的一些工具类型,就像在映射类型中介绍的可以通过 Partial<T>,可以在项目中直接使用。

  • Exclude<T, U> – 从 T 中剔除可以赋值给 U 的类型。
  • Extract<T, U> – 提取 T 中可以赋值给 U 的类型。
  • NonNullable<T> – 从 T 中剔除 null 和 undefined。
  • ReturnType<T> – 获取函数返回值类型。
  • InstanceType<T> – 获取构造函数类型的实例类型。

用第一个来举例分析:

type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>  // 'b' | 'd'
  • 1

来看一下 Exclude<T, U> 的实现源码:

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;
  • 1
  • 2
  • 3
  • 4

再看一个进阶的例子,定义一种方法,可以取出接口类型中的函数类型:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>

interface Part {
  id: number
  name: string
  subparts: Part[]
  firstFn: (brand: string) => void,
  anotherFn: (channel: string) => string
}

type FnNames = FunctionPropertyNames<Part>
type FnProperties = FunctionProperties<Part>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上面代码中,倒数第二行,首先,遍历整个接口,然后通过条件类型判断接口的属性值的类型是否是函数类型,如果是函数类型,取其属性名。得到:

type FnNames = 'firstFn' | 'anotherFn'
  • 1

倒数第一行,通过上一节介绍的工具函数 Pick,拿到这个接口的所有函数类型成员集合:

type FnProperties = {
  firstFn: (brand: string) => void
  anotherFn: (channel: string) => string
}
  • 1
  • 2
  • 3
  • 4

9、迭代器(Iterator)

9.1、基本概念

如果要从一个数据集中获取一个数据项,可以对这个数据集进行迭代。

JavaScript 提供了许多迭代集合的方法,从简单的 for 循环到 map()filter()。本节要介绍的迭代器也是一种方案,并且迭代器将迭代的概念直接带入核心语言,同时提供了一种机制来自定义 for...of 循环的行为。

迭代器是一种特殊对象,它符合迭代器协议规范。在 TypeScript 中,我们可以定义一个接口,这个接口上有一个函数类型 nextnext() 方法的返回值类型是 { value: any, done: boolean }。其中,value 是 any 类型,表示下一个将要返回的值;done 是布尔类型,当没有更多可返回数据时返回 true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置。

迭代器一旦创建,迭代器对象就可以通过重复调用 next() 显式地迭代。

9.2、模拟一个简单的迭代器

代码示例:

interface IteratorInterface {
  next: () => {
    value: any
    done: boolean
  }
}

function createIterator(array: any[]): IteratorInterface {
  let index = 0
  let len = array.length

  return {
    next: function () {
      return index < len ? { value: array[index++], done: false } : { value: undefined, done: true }
    }
  }
}

var iterator = createIterator([1, 2, 3])

console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在上面代码中,第 1 行,声明了一个 Iterator 接口,具有 next 这样一个函数类型。

第 8 行,声明了一个可以返回迭代器对象的函数,这个函数的返回值类型必须符合 Iterator 接口。

倒数第 4 行,通过调用迭代器对象上的 next() 方法,可以拿到数据集中的下一个数据项。

最后一行,拿到数据集中的所有数据后,done 属性变为 true

9.3、可迭代性

上面的例子,用模拟的迭代器地迭代了一个数组对象,那是不是所有的对象都可以这样迭代呢?当然不是。

只有一个对象实现了 Symbol.iterator 属性时,我们才认为它是可迭代的。一些内置的类型如 Array,Map,Set,String,Int32Array,Uint32Array 等都已经实现了各自的 Symbol.iterator

Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的迭代器生成函数。执行这个函数,就会返回一个迭代器。

比如,String 是一个内置的可迭代对象:

let str: string = 'Hi'
console.log(typeof str[Symbol.iterator]) // function
  • 1
  • 2

String 的默认迭代器会依次返回该字符串的字符:

let str: string = 'Hi'
let iterator: IterableIterator<string> = str[Symbol.iterator]()
 
console.log(iterator.next())      // { value: 'H', done: false }
console.log(iterator.next())      // { value: 'i', done: false }
console.log(iterator.next())      // { value: undefined, done: true }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在上面代码中,第 1 行,声明一个字符串类型变量,字符串类型内置了默认迭代器生成函数 Symbol.iterator

第 2 行,执行这个函数,返回了一个迭代器。

总结:

  • 为各种数据结构(ArrayMapSetString等),提供一个统一的、简便的访问接口。
  • 使得数据结构的成员能够按某种次序排列。
  • 创造了一种新的遍历命令 for..of 循环。

9.4、for...of

for...of 会遍历可迭代的对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等),调用对象上的 Symbol.iterator 方法。

迭代数组

let iterable = [10, 20, 30]

for (const value of iterable) {
  console.log(value)
}
// 10
// 20
// 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通过 for...of 循环遍历数组 iterable 的每一项元素。

const heroes = [
  {
    name: '艾希',
    gender: 2
  },
  {
    name: '泰达米尔',
    gender: 1
  }
]

for (let { name } of heroes) {
  console.log(name)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

通过 let { name } of heroes 循环迭代 heroes 对象数组,将每一个对象解构,得到每一个对象的 name 属性值。

迭代字符串

let iterable = 'hello'

for (const s of iterable) {
  console.log(s)
}
// i
// m
// o
// o
// c
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

字符串具有可迭代性,通过 for...of 可以快速遍历出每一个字符。

迭代 Map

let iterable = new Map()

iterable.set('a', 1)
iterable.set('b', 2)
iterable.set('c', 3)

for (let entry of iterable) {
  console.log(entry)
}
// ['a', 1]
// ['b', 2]
// ['c', 3]

for (let [key, value] of iterable) {
  console.log(value)
}
// 1
// 2
// 3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

一个 Map 对象在迭代时会根据对象中元素的插入顺序来进行。for...of 循环在每次迭代后会返回一个形式为 [key,value] 的数组。通过使用 let [key, value] 这种解构形式,可以快速获取每一项属性值。

9.5、for...offor...in 的区别

  • for...of 语句遍历可迭代对象定义要迭代的数据。
  • for...in 语句以任意顺序迭代对象的可枚举属性
let iterable: number[] = [3, 5, 7]

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i)
  }
}
// 0
// 1
// 2

for (let i of iterable) {
  console.log(i)
}
// 3
// 5
// 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

for...in 可以操作任何对象,迭代对象的可枚举属性。但是 for...of 只关注于可迭代对象的值。

9.6、解构赋值与扩展运算符

对数组和 Set 结构进行解构赋值时,会默认调用 Symbol.iterator 方法:

let [head, ...tail] = [1, 2, 3, 4]
// tail = [2, 3, 4]
  • 1
  • 2

扩展运算符也会调用默认的 Iterator 接口,得到一个数组结构:

let arr = [...'hello']
console.log(arr) //  ['i','m','o','o','c']
  • 1
  • 2

10、生成器(Generator)

10.1、概念

通过 function* 来创建一个生成器函数,在调用一个生成器函数后,并不会立即执行函数中的代码,而是会返回一个迭代器对象,通过调用迭代器对象的 next() 方法,可以获得 yield/return 的返回值。

10.2、生成器函数的特殊性

一个正常的函数,如果没有 return 或者 throw 一个异常,一旦被调用在运行结束之前是不会停止的。如果再次调用这个函数,它会再次从第一行开始执行。

function normalFunc() {
  console.log('I')
  console.log('cannot')
  console.log('be')
  console.log('stopped.')
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

相反,生成器函数可以中途停止,然后从停止的地方继续执行的。

生成器函数会返回一个对象,可以调用这个对象上的 next() 方法。

代码示例:

function* generatorFunction() { 
  console.log('开始执行')
  yield 'Hello, '

  console.log('暂停后再次执行')
  yield 'World!'
}

let iterator = generatorFunction()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

此时,通过 function* 语法创建了一个生成器函数,调用这个函数并赋值给变量 iterator,我们已经知道这是个对象。

console.log(iterator.next().value)
// 开始执行
// Hello, 
  • 1
  • 2
  • 3

调用 iterator 对象上的 next() 方法,首先打印出 开始执行,然后遇到了 yield Hello,yield 会将后面的值返回,生成器生成一个对象 { value: 'Hello, ', done: false },函数停止运行,直到再次调用 next() 方法。

console.log(iterator.next().value)
// 暂停后再次执行
// World!
  • 1
  • 2
  • 3

再次调用 next() 方法,函数内继续执行,打印出 暂停后再次执行,遇到 yield 'World!',生成对象 { value: 'World!', done: false },函数停止运行,直到再次调用 next() 方法。

console.log(iterator.next())
  • 1

再次调用 next() 方法,这次函数内没有返回值,也就是默认返回 undefined, 生成对象 { value: 'undefined', done: true }

10.3、通过 next() 参数向生成器传值

在调用 next() 的时候可以传递一个参数,在上次 yield 前接收到这个参数:

function* gen() { 
  console.log('开始执行')
  let res1 = yield 1
  console.log('中断后继续执行')
  console.log(res1)
  
  let res2 = yield 2
  console.log(res2)
  
  console.log('执行结束')
  return 3
}

let iterator = gen()
console.log(iterator.next('first'))
console.log(iterator.next('second'))
console.log(iterator.next('third'))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

执行并查看结果:

开始执行
{ value: 1, done: false }
中断后继续执行
second
{ value: 2, done: false }
third
执行结束
{ value: 3, done: true }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里注意下,生成器最初没有产生任何结果,在第一次调用 next() 时传参是无意义的。

11、装饰器(Decorator)

11.1、基本概念

装饰器是一种特殊类型的声明,它能够附加到类声明、方法、访问符、属性、类方法的参数上,以达到扩展类的行为。

自从 ES2015 引入 class,当我们需要在多个不同的类之间共享或者扩展一些方法或行为的时候,代码会变得错综复杂,极其不优雅,这也是装饰器被提出的一个很重要的原因。

常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器。

装饰器的写法:普通装饰器(无法传参)、 装饰器工厂(可传参)。

若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:

命令行:

tsc --target ES5 --experimentalDecorators
  • 1

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}                                                                          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

11.2、装饰器的使用方法

装饰器允许你在类和方法定义的时候去注释或者修改它。装饰器是一个作用于函数的表达式,它接收三个参数 targetnamedescriptor ,然后可选性的返回被装饰之后的 descriptor 对象。

装饰器使用 @expression 这种语法糖形式,expression 表达式求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器工厂

装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。

通过装饰器工厂方法,可以额外传参,普通装饰器无法传参

function log(param: string) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    console.log('target:', target)
    console.log('name:', name)
    console.log('descriptor:', descriptor)

    console.log('param:', param)
  }
}

class Employee {

  @log('with param')
  routine() {
    console.log('Daily routine')
  }
}

const e = new Employee()
e.routine()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在上面代码中,第 1 行,声明的 log() 函数就是一个装饰器函数,通过装饰器工厂这种写法,可以接收参数。

来看代码的打印结果:

target: Employee { routine: [Function] }
name: routine
descriptor: {
  value: [Function],
  writable: true,
  enumerable: true,
  configurable: true
}
param: with param
Daily routine
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到,先执行装饰器函数,然后执行 routine() 函数。至于类属性装饰器函数表达式的三个参数 targetnamedescriptor 之后会单独介绍。

装饰器组合

多个装饰器可以同时应用到一个声明上,就像下面的示例:

  • 书写在同一行上:
@f @g x
  • 1
  • 书写在多行上:
@f
@g
x
  • 1
  • 2
  • 3

TypeScript 里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值
  2. 求值的结果会被当作函数,由下至上依次调用

通过下面的例子来观察它们求值的顺序:

function f() {
  console.log('f(): evaluated');
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('f(): called');
  }
}

function g() {
  console.log('g(): evaluated');
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log('g(): called');
  }
}

class C {
  @f()
  @g()
  method() {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在控制台里会打印出如下结果:

f(): evaluated
g(): evaluated
g(): called
f(): called
  • 1
  • 2
  • 3
  • 4

11.3、类装饰器

类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

通过类装饰器扩展类的属性和方法:

function extension<T extends { new(...args:any[]): {} }>(constructor: T) {
  // 重载构造函数
  return class extends constructor {
    // 扩展属性
    public coreHour = '10:00-15:00'
    // 函数重载
    meeting() {
      console.log('重载:Daily meeting!')
    }
  }
}

@extension
class Employee {
  public name!: string
  public department!: string

  constructor(name: string, department: string) {
    this.name = name
    this.department = department
  }

  meeting() {
    console.log('Every Monday!')
  }

}

let e = new Employee('Tom', 'IT')
console.log(e) // Employee { name: 'Tom', department: 'IT', coreHour: '10:00-15:00' }
e.meeting()    // 重载:Daily meeting!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

函数表达式的写法:

const extension = (constructor: Function) => {
  constructor.prototype.coreHour = '10:00-15:00'

  constructor.prototype.meeting = () => {
    console.log('重载:Daily meeting!');
  }
}

@extension
class Employee {
  public name!: string
  public department!: string

  constructor(name: string, department: string) {
    this.name = name
    this.department = department
  }

  meeting() {
    console.log('Every Monday!')
  }

}

let e: any = new Employee('Tom', 'IT')
console.log(e.coreHour) // 10:00-15:00
e.meeting()             // 重载:Daily meeting!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

以上两种写法,其实本质是相同的,类装饰器函数表达式将构造函数作为唯一的参数,主要用于扩展类的属性和方法。

11.4、作用于类属性的装饰器

作用于类属性的装饰器表达式会在运行时当作函数被调用,传入下列3个参数 targetnamedescriptor

  1. target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. name: 成员的名字
  3. descriptor: 成员的属性描述符

如果你熟悉 Object.defineProperty,你会立刻发现这正是 Object.defineProperty 的三个参数。

比如通过修饰器完成一个属性只读功能,其实就是修改数据描述符中的 writable 的值 :

function readonly(value: boolean) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    descriptor.writable = value
  }
}

class Employee {
  @readonly(false)
  salary() {
    console.log('这是个秘密')
  }
}

const e = new Employee()
e.salary = () => { // Error,不可写
  console.log('change')
}
e.salary()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

因为 readonly 装饰器将数据描述符中的 writable 改为不可写,所以倒数第三行报错。

11.5、方法参数装饰器

参数装饰器表达式会在运行时当作函数被调用,以使用参数装饰器为类的原型上附加一些元数据,传入下列3个参数 targetnameindex

  1. target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. name: 成员的名字
  3. index: 参数在函数参数列表中的索引

注意第三个参数的不同。

function log(param: string) {
  console.log(param)

  return function (target: any, name: string, index: number) {
    console.log(index)
  }
}

class Employee {

  salary(@log('IT') department: string, @log('John') name: string) {
    console.log('这是个秘密')
  }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以用参数装饰器来监控一个方法的参数是否被传入。

11.6、装饰器执行顺序

function extension(params: string) {
  return function (target: any) {
    console.log('类装饰器')
  }
}

function method(params: string) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    console.log('方法装饰器')
  }
}

function attribute(params: string) {
  return function (target: any, name: string) {
    console.log('属性装饰器')
  }
}

function argument(params: string) {
  return function (target: any, name: string, index: number) {
    console.log('参数装饰器', index)
  }
}

@extension('类装饰器')
class Employee{
  @attribute('属性装饰器')
  public name!: string

  @method('方法装饰器')
  salary(@argument('参数装饰器') name: string, @argument('参数装饰器') department: string) {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

查看运行结果:

属性装饰器
参数装饰器 1
参数装饰器 0
方法装饰器
类装饰器
  • 1
  • 2
  • 3
  • 4
  • 5

13、TypeScriptReact 项目中的应用

13.1、创建支持 TypeScriptReact 项目

执行下面的命令新建一个名字为 demoReact 项目:

npx create-react-app demo --template typescript
  • 1

根据 typescript 官网文档的说明,还可以使用下面的命令:

npx create-react-app demo --scripts-version=react-scripts-ts
  • 1

react-script-ts是一个在采用了标准的 create-react-app 项目流程 的基础上,混合了 TypeScript 的功能的集合。

我想,原来是采用的第二种方式,后来就整理为了第一种方式,这里采用第一种方式。

创建的项目文件结构:

demo02-ts-template/
  |─   node_modules/
  |─   public/
  |      └─ favicon.ico
  |      └─ index.html
  |      └─ manifest.json
  |      └─ ...
  |─   src/
  |      └─ ...
  |─   .gitignore
  |─   package.json
  |─   package-lock.json
  |─   README.md
  └─   tsconfig.json
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

执行:

npm start
  • 1

运行项目,默认服务将运行在 localhost:3000

13.2、tsconfig.json 配置文件详解

如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录,tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。

一个项目可以通过以下方式之一来编译:

  • 不带任何输入文件的情况下调用 tsc 命令,编译器会从当前目录开始去查找 tsconfig.json 文件,逐级向上搜索父目录。
  • 不带任何输入文件的情况下调用 tsc 命令,且使用命令行参数 --project(或 -p )指定一个包含 tsconfig.json 文件的目录。

当命令行上指定了输入文件时,tsconfig.json 文件会被忽略。

一个 tsconfig.json 文件主要有以下配置项:

{
  "compilerOptions": {},
  "files": [],
  "include": [],
  "exclude": [],
  "extends": "",
  "compileOnSave": false,
  "typeAcquisition": {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

compilerOptions

compilerOptions:对象类型,用来设置编译选项,若不设置则默认使用上节介绍的默认配置。

下面是一份梳理的常用 compilerOptions 属性配置:

{
  "compilerOptions": {
    "target": "esnext", /* 指定编译之后的版本目标: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "esnext", /* 指定要使用的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "noImplicitAny": false, /* 是否默认禁用 any */
    "removeComments": true, /* 是否移除注释 */
    "declaration": true, /* 是否自动创建类型声明文件 */
    "strict": true, /* 启动所有类型检查 */
    "jsx": "preserve", /* 指定jsx代码用于的开发环境 */
    "importHelpers": true, /* 引入tslib里的辅助工具函数*/
    "moduleResolution": "node", /* 选择模块解析策略,有'node'和'classic'两种类型 */
    "experimentalDecorators": true, /* 启用实验性的装饰器特性 */
    "esModuleInterop": true,  /* 通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性 */
    "allowSyntheticDefaultImports": true, /* 允许从没有默认导出的模块中默认导入 */
    "sourceMap": true, /* 是否生成map文件 */
    "baseUrl": ".", /* 工作根目录 */
    "types": [ /* 指定引入的类型声明文件,默认是自动引入所有声明文件,一旦指定该选项,则会禁用自动引入,改为只引入指定的类型声明文件,如果指定空数组[]则不引用任何文件 */
      "webpack-env",
      "jest"
    ],
    "paths": { /* 指定模块的路径,和 baseUrl有关联,和 webpack 中 resolve.alias 配置一样 */
      "@/*": [
        "src/*"
      ]
    },
    "lib": [ /* 译过程中需要引入的库文件的列表 */
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

filesincludeexclude

  • iles 是一个数组列表,写入待编译文件的相对或绝对路径,不支持 glob 匹配模式。
  • include 是一个数组列表,写入待编译文件的路径,支持 glob 匹配模式。
  • exclude 也是一个数组列表,写入排除某些文件路径,这些文件排除于待编译列表,支持 glob 匹配模式。

glob 通配符有:

  • * 匹配 0 或多个字符(不包括目录分隔符)
  • ? 匹配一个任意字符(不包括目录分隔符)
  • **/ 递归匹配任意子目录

如果 "files""include" 都没有被指定,编译器默认包含当前目录和子目录下所有的 TypeScript 文件(.ts, .d.ts.tsx),排除在"exclude" 里指定的文件。

如果开启了 allowJs 选项,那 .js.jsx 文件也属于编译器包含范围。

{
  "files": [
  "core.ts",
  "index.ts",
  "types.ts"
  ],
  "exclude": [
    "node_modules", 
    "lib", 
    "**/*.test.ts"
  ],
  "include": [
    "src/**/*"
  ],
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

如果没有特殊指定,"exclude" 默认情况下会排除 node_modules,bower_components,jspm_packages<outDir> 目录。

任何被 "files""include" 指定的文件所引用的文件也会被包含进来。

优先级:命令行配置 > files > exclude > include

extends

extends:字符串类型,指向另一个要继承文件的路径。例如:

{
  "extends": "config/base.json"
}
  • 1
  • 2
  • 3

这个配置项的意思就是我们可以借助 "extends" 属性引入路径为 "config/base.json" 的配置文件中的配置选项。

configs/base.json:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

需要注意:

  • 如果有同名配置,继承文件里的配置会覆盖源文件里的配置

compileOnSave

compileOnSave 是一个布尔类型的属性,当值为 true 时,设置 compileOnSave 属性到 IDE,以便 tsconfig.ts 文件在保存时能够重新生成文件。

typeAcquisition

typeAcquisition:对象类型,用以设置自动引入库类型定义文件(.d.ts),该属性下面有3个子属性:

  • enable: 布尔类型,用以设置是否开启自动引入库类型定义文件
  • include: 数组类型,允许自动引入的库名列表,如 ["jquery", "kendo-ui"]
  • exclude: 数组类型,排除的库名列表

@typestypeRootstypes

默认情况下,node_modules/@types 文件夹下以及它们子文件夹下的所有包都会在编译过程中被包含进来。

但是如果指定了 typeRoots,则只有 typeRoots 路径下的包才会被包含进来:

{
  "compilerOptions": {
    "typeRoots" : ["./typings"]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5

这个配置文件会包含所有 ./typings 下面的包,而不包含 ./node_modules/@types 里面的包。

如果指定了 types,只有被列出来的包才会被包含进来。比如:

{
  "compilerOptions": {
    "types": ["node", "lodash", "express"]
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5

如果 types 设置为空数组,则禁止自动引入 @types 包:

{
  "compilerOptions": {
    "types": []
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5

注意,自动引入只在你使用了全局的声明(相反于模块)时是重要的。如果你使用 import "foo" 语句,TypeScript 仍然会查找 node_modulesnode_modules/@types 文件夹来获取 foo 包。

13.3、创建第一个 Hello.tsx 组件

在创建好的项目中,简化 src 目录下的内容,删除 src/index.tsx 之外的所有文件,将 src/index.tsx 文件的内容做精简,示例代码如下:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <div>
  </div>
,document.getElementById('root'));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

创建 src/Hello.tsx 文件,示例代码如下:

import React from 'react'

interface IProps {
    message: string
}

const Hello = (props:IProps) => {
    return <h2>{props.message}</h2>
}

export default Hello
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Hello.tsx 组件引入到 index.tsx 入口文件中,示例代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import Hello from './Hello';

ReactDOM.render(
  <div>
    <Hello message="hello world" />
  </div>
,document.getElementById('root'));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在终端执行 npm start 启动项目。

对上面代码还可以继续做优化。修改 src/Hello.tsx 组件,示例代码如下:

import React from 'react'

interface IProps {
    message?: string
}

const Hello:React.FunctionComponent<IProps> = (props) => {
    return <h2>{props.message}</h2>
}

Hello.defaultProps = {
    message: 'hello world'
}

export default Hello
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在上面代码中,声明函数组件时使用 React.FunctionComponent 接口,该接口接受一个泛型,使用代码中定义的 IProps 接口作为泛型,此时的 Hello 组件中就可以接收 props.children 的属性,用于接收组件实例中传入的子节点。同时,还可以使用 defaultPropsprops 对象中的属性设置初始化值。

在组件中引入 React.FunctionComponent 单词有点长,可以使用简写的方式引入,修改后的代码如下:

const Hello:React.FC<IProps> = (props) => {
    return <h2>{props.message}</h2>
}
  • 1
  • 2
  • 3

在上面代码中,React.FC 就是 React.FunctionComponent 的简写。

13.4、自定义 Button 组件

安装 classnames 工具:

$ npm install classnames --save
$ npm install @types/classnames --save
  • 1
  • 2

创建 src/components/Button/button.tsx 组件,示例代码如下:

import React from 'react';
import classNames from 'classnames';
import './button.css';

// 声明按钮尺寸枚举
export enum ButtonSize {
    Large = 'lg',
    Small = 'sm'
}

// 声明按钮样式枚举
export enum ButtonType {
    Primary = 'primary',
    Default = 'default',
    Danger = 'danger',
    Link = 'link'
}

// 声明按钮组件的 props 接口
interface BaseButtonProps {
    className?: string;
    disabled?: boolean;
    size?: ButtonSize;
    btnType?: ButtonType;
    children?: React.ReactNode;
    href?: string
}

// 声明按钮与超链接标签的原生事件
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
    
const Button:React.FC<ButtonProps> = (props) => {
    const {
        className,
        btnType,
        disabled,
        size,
        children,
        href,
        ...restProps // 解构按钮与超链接的原生事件属性
    } = props

    // 使用 classnames 工具拼接样式的 class 值
    let classes = classNames('btn',className,{
        [`btn-${btnType}`]: btnType,
        [`btn-${size}`]:size
    })

    if(btnType === ButtonType.Link && href) {
        return (
            <a
               className={classes}
               href={href}
               {...restProps}
            >
                   {children}
            </a>
        )
    } else {
        return (
            <button
                className={classes}
                disabled={disabled}
                {...restProps}
            >
                {children}
            </button>
        )
    }
}

// 定义 props 的默认值
Button.defaultProps = {
    disabled: false,
    btnType: ButtonType.Default,
    size: ButtonSize.Small
}

export default Button
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

创建 src/components/Button/button.css 样式文件,示例代码如下:

.btn {
    width: 80px;
    height: 30px;
    font-size: 16px;
    color: #666;
}

.btn-danger {
    color: #f56;
}

.btn-primary {
    color: #37f;
}

.btn-lg {
    width: 150px;
    height: 50px;
    font-size: 22px;
}

.btn-sm {
    width: 60px;
    height: 20px;
    font-size: 14px;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

以上 css的值仅供参考

src/App.tsx 组件中引入 Button 组件,示例代码如下:

import React from 'react'
import Button, {ButtonType,ButtonSize} from './components/Button/button.tsx';
import './app.css'

const App:React.FC = () => {
  return (
    <>
      <Button disabled>Hello</Button>
      <Button btnType={ButtonType.Primary} size={ButtonSize.Large}>Hello</Button>
      <Button btnType={ButtonType.Danger} size={ButtonSize.Small}>Hello</Button>
      <Button btnType={ButtonType.Link} href="http://www.baidu.com">百度一下</Button>
      <Button className='my-btn'>hello</Button>
    </>
  )
}

export default App
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

创建 src/app.css 样式文件,示例代码如下:

.my-btn{
    width: 300px;
    height: 80px;
    color: 'pink';
    font-size: 15px;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

以上 css 的值仅供参考

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/174375
推荐阅读
相关标签
  

闽ICP备14008679号