一、TypeScript介绍

TypeScript 由微软开发的自由和开源的编程语言。

TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准。

TypeScript使用现有的JavaScript代码,包括流行的JavaScript库,并从JavaScript代码中调用TypeScript代码。

TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。

TypeScript可以在任何浏览器、任何计算机和任何操作系统上运行,且是开源的。

TypeScript 是一种面向对象的编程语言。

TypeScript 遵循强类型,如果将不同的类型赋值给变量会编译错误。

1.1 TypeScript 的优缺点

优点

  •     增加代码的阅读性,大部分函数看类型定义就知道如何使用。
  •     编辑阶段会发现大部分问题。
  •     与编辑器有良好的增强,例如代码补全、接口提示等。
  •     即使编译报错,也可以生产对应的 JavaScript 文件。
  •     兼容第三方库。
  •     TypeScript 遵循强类型,如果将不同的类型赋值给变量会编译错误

缺点

  •     有一定的学习成本,需要理解接口、泛型、类、枚举类型等概念。
  •     前期开发有额外的成本。
  •     集成到构建流程需要额外工作量。

js问题

  •     JS只有在运行时,才会抛出错误,
  •     JS里有任何的拼写错误都不会提示错误
  •     JS运行时报的错,指向也未必是错误的源头,也就是说:A类里因为书写代码有误,会造成运行时其他的类报错如B
  •     JS无法做到不同package的同名类

TS对JS的改进主要是静态类型检查,静态类型检查有何意义?标准答案是“静态类型更有利于构建大型应用”。为什么静态类型有利于构建大型应用?我总结,利在两点:

其一,静态类型检查可以做到early fail,即你编写的代码即使没有被执行到,一旦你编写代码时发生类型不匹配,语言在编译阶段(解释执行也一样,可以在运行前)即可发现。针对大型应用,测试调试分支覆盖困难,很多代码并不一定能够在所有条件下执行到。而假如你的代码简单到任何改动都可以从UI体现出来,这确实跟大型应用搭不上关系,那么静态类型检查确实没什么作用。

其二,类型就是最好的注释。静态类型对阅读代码是友好的,比如我们举个例子 jQuery API Documentation 这是大家都非常喜欢用的jQuery.ajax,在这份文档中,详尽地解释了类型为object的唯一一个参数settings,它是如此之复杂,如果没有文档,我们只看这个函数声明的话,根本不可能有人把这个用法猜对。针对大型应用,方法众多,调用关系复杂,不可能每个函数都有人编写细致的文档,所以静态类型就是非常重要的提示和约束。而假如你的代码像jQuery这样所有函数基本全是API,根本没什么内部函数,而且逻辑关系看起来显而易见,这确实跟大型应用搭不上关系,那么静态类型对阅读代码确实也没什么帮助。总的来说,现代编程语言设计,很多特性已经有非常成熟的理论支持了,如果我们重视计算机基础,那么一些语言的适用场景就像是拼积木,可以用几句话概括。像是TS对JS这样,只是单一特性变化。

类型注释:

    //注释为string
    let username: string = 'muzidigbig';
    let content: string = `Hello, my name is ${username}`;
    //注释为array
    let arr: number[] = [1,2,3];

1.2 使用Typescript的原因

Javascript是一门弱类型语言,变量的数据类型具有动态性,只有执行的时候才能确定变量的类型,这种后知后觉的认错方法会让开发者成为调试大师,但无益于编程能力的提升,还会降低开发效率。Typescript的类型机制可以有效的杜绝变量类型引起的误用问题,而且开发者可以根据情况来确定是严格限制变量类型还是宽松限制。不过,添加类型限制后,也有副作用:增大了开发者的学习曲线,增加了设定类型的开发时间,但这些付出相对于代码的健壮性和可维护性,都是值得的。

1.3 快速了解 TypeScript

NPM 安装 TypeScript

如果你的本地环境已经安装了 npm 工具,可以使用以下命令来安装:

npm install -g typescript

安装完成后我们可以使用 tsc 命令来执行 TypeScript 的相关代码,以下是查看版本号:

    $ tsc -v
    Version 3.2.2

构建第一个 TypeScript 文件

在编辑器内创建一个 hello.ts 文件,这里推荐使用 Visual Studio Code 编辑器

    function sayHello(person) {
        return 'Hello, ' + person;
    }
     
    let user = 'muzidigbig';
     
    console.log(sayHello(user));

通常我们使用 .ts 作为 TypeScript 代码文件的扩展名。

然后执行以下命令将 TypeScript 转换为 JavaScript 代码:

tsc hello.ts

这时候再当前目录下(与 hello.ts 同一目录)就会生成一个 hello.js 文件,代码如下:

    function sayHello(person) {
        return 'Hello, ' + person;
    }
    var user = 'muzidigbig';
    console.log(sayHello(user));

使用 node 命令来执行 hello.js 文件(或者在页面中直接引用这个hello.js文件):

    $ node hello.js
    Hello, muzidigbig

TypeScript 转换为 JavaScript 过程如下图:

1.4 初始TS

- 类型声明

  - 类型声明是TS非常重要的一个特点

  - 通过类型声明可以指定TS中变量(参数、形参)的类型

  - 指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错

  - 简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值

  - 语法:

    - ```typescript

      let 变量: 类型;

      let 变量: 类型 = 值;

      function fn(参数: 类型, 参数: 类型): 类型{

          ...

      }

      ```

- 自动类型判断

  - TS拥有自动的类型判断机制

  - 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型

  - 所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明

类型注解

在 TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。在下面的例子里,我们将 sayHello 函数接收一个字符串参数,然后将对 sayHello 的调用改为传入一个数组对象。

    function sayHello(person: string) {
        return 'Hello, ' + person;
    }
     
    let user = [1,2,3,4,5];
    console.log(sayHello(user));

重新执行编辑后,会发现产生了一个报错信息:

hello.ts:7:22 - error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

TypeScript 提供了静态的代码分析,分析代码结构和提供的类型注解,尽管有报错,对应的 hello.js 文件依旧被创建

    //正确写法
    let user = '[1,2,3,4,5]';

接口

  1. TypeScript 的核心原则之一是对值所具有的结构(属性、参数等)进行类型检查。我们使用接口(Interfaces)来定义对象的类型。
  2. 接口是对象的状态(属性)和行为(方法)的抽象(约束)
  3. 接口:是一种类型,是一种规范,是一种能力,是一种约束

可以将接口理解为给变量添加约束

接口可以在定义类的时候去限制类的结构;接口中的所有的属性都不能有实际的值;接口只定义对象的结构,而不考虑实际值;在接口中所有的方法都是抽象方法。

继续改动这个项目,我们通过接口来描述一个拥有 firstname 和 lastname 字段的对象。在 TypeScript 中,只要两个类型内部的结构兼容,那么这两个类型就是兼容的。

    //接口关键字 interface
    interface Person {
        firstName: string;
        lastName: string;
    }
     
    function sayHello(person: Person) {
        return 'Hello, ' + person.firstName + ' ' + person.lastName;
    }
     
    let user = { firstName: 'Lee', lastName: 'muzidigbig' };
     
    console.log(sayHello(user));

类描述了所创建的对象共同的属性和方法。TypeScript 支持面向对象的所有特性,比如 类、接口等。

最后,让我们使用类来改写这个例子,TypeScript 支持 JavaScript 的新特性,比如支持基于类的面向对象编程。让我们创建一个 Student 类,它带有一个构造函数和一些公共字段,注意接口和类可以一起工作,这个要由我们决定抽象的级别。

    class Student {
        // 定义属性
        fullName: string;

        // 定义构造函数:为了将来实例化对象的时候,可以直接对属性的值进行初始化
        constructor(public firstName: string, public lastName: string) {
            this.fullName = firstName + ' ' + lastName;
        }
    }
     
    interface Person {
        firstName: string;
        lastName: string;
    }
     
    function sayHello(person: Person) {
        return 'Hello, ' + person.firstName + ' ' + person.lastName;
    }
     
    let user = new Student('Lee', 'muzidigbig');
     
    console.log(sayHello(user)); // Hello, Lee muzidigbig

重新运行编辑

tsc hello.ts

生成一个新的 hello.js 文件

    var Student = /** @class */ (function () {
        function Student(firstName, lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.fullName = firstName + ' ' + lastName;
        }
        return Student;
    }());
    function sayHello(person) {
        return 'Hello, ' + person.firstName + ' ' + person.lastName;
    }
    var user = new Student('Lee', 'muzidigbig');
    console.log(sayHello(user));

二、TypeScript 数据类型 

2.1 Boolean 类型

let isDone: boolean = false;
// ES5:var isDone = false;

2.2 Number 类型

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

        其中 0b1010 和 0o744 是 ES6 的二进制与八进制的表示法,它们会被编译为十进制的数字。 

2.3 String 类型

let name: string = "semliker";
// ES5:var name = 'semlinker';

2.4 Symbol 类型

自ECMAScript 2015起,symbol成为了一种新的原生类型,就像 numberstring 一样。

symbol类型的值是通过Symbol构造函数创建的。

 Symbol是不可改变且唯一的。

const sym = Symbol();
let obj = {
  [sym]: "semlinker",
};

console.log(obj[sym]); // semlinker 

2.5 Array 类型

// 方式一:「类型 + 方括号」表示法,数组的项中不允许出现其他的类型:
// 在元素类型后面加上[]
let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];
console.log(list instanceof Array); //true

// 方式二:使用数组泛型:
let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3];

// any 在数组中的应用
// 一个比较常见的做法是,用 any 表示数组中允许出现任意类型:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

用接口来描述数组

interface Arr{
    [index:number]:string  //[下标:下标类型]:值类型
}
let arr:Arr[] = ["1","str","hello"];
console.log(arr,arr[1]); // ['1', 'str', 'hello'] 'str'

 2.6 Tuple 类型

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

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

元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:

let tupleType: [string, boolean];
tupleType = ["semlinker", true];

在上面代码中,我们定义了一个名为 tupleType 的变量,它的类型是一个类型数组 [string, boolean],然后我们按照正确的类型依次初始化 tupleType 变量。与数组一样,我们可以通过下标来访问元组中的元素:

console.log(tupleType[0]); // semlinker
console.log(tupleType[1]); // true

在元组初始化的时候,如果出现类型不匹配的话,比如:

tupleType = [true, "semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.

很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:

tupleType = ["semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

Property '1' is missing in type '[string]' but required in type '[string, boolean]'.

越界元素:当访问超出元祖长度的元素时,它的类型会被限制为元祖中每个类型的联合类型。 

let arr:[string, number] = ['name', 20];
arr[0] = 'age';
arr[2] = 'string';
arr[3] = 40;
arr[4] = true; // 编译报错

2.7 Enum 类型

枚举是对 js 标准数据类型的补充,声明一组带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

简言之 enum 枚举,几个值之间取值(固定范围中取值)

1.数字枚举

// 利用 const 关键词也可以声明一组常量,例如,声明十二生肖的排位
const NORTH: number = 0
const SOUTH: number = 1
const EAST: number = 2
const rabbit: number = 3
// 上述只声明了 4 个

// 类型,但是却多了很多重复性工作,利用数字枚举,我们可以轻松声明同样的一组常量
enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;
// 如果想访问 NORTH,只需要像 js 中对象使用点语法或者中括号访问对象成员即可
Direction.NORTH === Direction['NORTH'] => true

默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。

以上的枚举示例经编译后,对应的 ES5 代码如下:

"use strict";
var Direction;
(function (Direction) {
  Direction[(Direction["NORTH"] = 0)] = "NORTH";
  Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
  Direction[(Direction["EAST"] = 2)] = "EAST";
  Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;

当然我们也可以设置 NORTH 的初始值,手动赋值时, 未赋值的枚举成员会接着上一个枚举项递增1(初始化),比如:

enum Weeks {
    Mon, Tue, Wed, Thu = 2, Fri, Sat = -1.5, Sun
};
 
console.log(Weeks['Mon']); // => 0
console.log(Weeks.Wed); // => 2
console.log(Weeks.Thu); // => 2
console.log(Weeks.Fri); // => 3
console.log(Weeks.Sun); // => -0.5

 上例中,未手动赋值的 Wed 和手动赋值的 Thu 取值重复了,但是 TypeScript 并不会报错,该种情况可能会引起取值错误,所以使用的时候最好避免出现取值重复的情况。 

2.字符串枚举

在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
}

以上代码对应的 ES5 代码如下:

"use strict";
var Direction;
(function (Direction) {
    Direction["NORTH"] = "NORTH";
    Direction["SOUTH"] = "SOUTH";
    Direction["EAST"] = "EAST";
    Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));

通过观察数字枚举和字符串枚举的编译结果,我们可以知道数字枚举除了支持 从成员名称到成员值 的普通映射之外,它还支持 从成员值到成员名称 的反向映射:

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; // 0

另外,对于纯字符串枚举,我们不能省略任何初始化程序。而数字枚举如果没有显式设置值时,则会使用默认规则进行初始化。

3.常量枚举

除了数字枚举和字符串枚举之外,还有一种特殊的枚举 —— 常量枚举。它是使用 const 关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript。为了更好地理解这句话,我们来看一个具体的例子:

const enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

以上代码对应的 ES5 代码如下:

"use strict";
var dir = 0 /* NORTH */;

当满足以下条件时,枚举成员被当作是常数:

    不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0。
    枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
        数字字面量
        引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
        带括号的常数枚举表达式
        +, -, ~ 一元运算符应用于常数枚举表达式
        +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符,常数枚举表达式做为其一个操作对象。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错

所有其它情况的枚举成员被当作是需要计算得出的值。

4.异构枚举

异构枚举的成员值是数字和字符串的混合:

enum Enum {
  A,
  B,
  C = "C",
  D = "D",
  E = 8,
  F,
}

以上代码对于的 ES5 代码如下:

"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
    Enum[Enum["B"] = 1] = "B";
    Enum["C"] = "C";
    Enum["D"] = "D";
    Enum[Enum["E"] = 8] = "E";
    Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));

通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:

console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A

5.外部枚举 declare enum

外部枚举与声明语句一样,常出现在声明文件中。

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
 
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

编译后:

var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

 同时使用 declareconst 也是可以的,编译结果同常量枚举一致。

2.8 Any 类型

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

let notSure: any = 666;
notSure = "semlinker";
notSure = false;

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。比如:

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

2.9 Unknown  表示未知类型的值(赋值给别的类型时会报错)

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下 unknown 类型的使用示例:

let value: unknown;

value = true; // OK
value = 42; // 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("type"); // OK

value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

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
let value8: Function = value; // Error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:

let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。

2.10 Void 类型

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:

// 声明函数返回值为void
function warnUser(): void {
  console.log("This is my warning message");
}

以上代码编译生成的 ES5 代码如下:

"use strict";
function warnUser() {
  console.log("This is my warning message");
}

需要注意的是,声明一个 void 类型的变量没有什么作用,因为在严格模式下,它的值只能为 undefined

let unusable: void = undefined;

需要注意一下,void 类型的变量不能赋值给其他类型的变量:

let u: void;
let num: number = u;
 
// error TS2322: Type 'void' is not assignable to type 'number'.

2.11 Null 和 Undefined 类型

TypeScript 里,undefinednull 两者有各自的类型分别为 undefinednull

let u: undefined = undefined;
let n: null = null;

只定义了变量未赋值默认被赋值为 undefined;null 类型的变量只能被赋值为 null,undefined 和 null 是所有类型的子类型。 

2.12 object, Object 和 {} 类型

1.object 类型

object 类型是:TypeScript 2.2 引入的新类型,它用于表示非原始类型。

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  create(o: object | null): any;
  // ...
}

const proto = {};

Object.create(proto);     // OK
Object.create(null);      // OK
Object.create(undefined); // Error
Object.create(1337);      // Error
Object.create(true);      // Error
Object.create("oops");    // Error

2.Object 类型

Object 类型:它是所有 Object 类的实例的类型,它由以下两个接口来定义:

  • Object 接口定义了 Object.prototype 原型对象上的属性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}
  • ObjectConstructor 接口定义了 Object 类的属性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;
  readonly prototype: Object;
  getPrototypeOf(o: any): any;
  // ···
}

declare var Object: ObjectConstructor;

Object 类的所有实例都继承了 Object 接口中的所有属性。

3.{} 类型

{} 类型描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误。

// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";

但是,你仍然可以使用在 Object 类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript 的原型链隐式地使用:

// Type {}
const obj = {};

// "[object Object]"
obj.toString();

2.13 Never  表示永远不会返回结果

never 类型表示的是其它类型(包括 null 和 undefined)的子类型,那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

永远不存在值的类型,一般用于错误处理函数。

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

type(类型别名) 关键字用来定义一种类型。

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean;

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保

controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

- & 表示同时

  - ```typescript

    // let j: string & number; // (无意义)

    // 使用

    let j2: { name: string } & { age: number };

    j2 = { name: "孙悟空", age: 18 }

    ```

2.14 声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能

什么是声明语句?

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $ 或 jQuery 了。

但是在 ts 中,编译器并不知道 $ 或 jQuery 是什么东西

/* 
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明语句: 如果需要ts对新的语法进行检查, 需要要加载了对应的类型说明代码
  declare var jQuery: (selector: string) => any;
声明文件: 把声明语句放到一个单独的文件(jQuery.d.ts)中, ts会自动解析到项目中所有声明文件
下载声明文件: npm install @types/jquery --save-dev
*/

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

这时,我们需要使用 declare var 来定义它的类型

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

jQuery('#foo');

 declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

jQuery('#foo');

一般声明文件都会单独写成一个 xxx.d.ts 文件

创建 01_jQuery.d.ts, 将声明语句定义其中, TS编译器会扫描并加载项目中所有的TS声明文件

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

很多的第三方库都定义了对应的声明文件库, 库文件名一般为 @types/xxx, 可以在 https://www.npmjs.com/package/package 进行搜索

// 最终方式
jQuery('#foo');

有的第三库在下载时就会自动下载对应的声明文件库(比如: webpack),有的可能需要单独下载(比如jQuery/react)

2.15 内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。

内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。

ECMAScript 的内置对象

Boolean、Number、String、Error、Date、RegExp 等。更多的内置对象,可以查看 MDN 的文档。

/* 1. ECMAScript 的内置对象 */
let b: Boolean = new Boolean(1)
let n: Number = new Number(true)
let s: String = new String('abc')
let d: Date = new Date()
let r: RegExp = /^1/
let e: Error = new Error('error message')
console.log(typeof b) // object
b = true
console.log(b) // true
// let bb: boolean = new Boolean(2)  // error

DOM 和 BOM 的内置对象

Document、HTMLElement、NodeList、Event、MouseEvent 等。

    let body: HTMLElement = document.body;
    let div: HTMLElement = document.getElementById('test')
    let allDiv: NodeList = document.querySelectorAll('div');
    document.addEventListener('click', function(e: MouseEvent) {
      // Do something
    });
    let fragment: DocumentFragment = document.createDocumentFragment()

三、TypeScript 断言

3.1 类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:

1.“尖括号” 语法

/* 
类型断言:告诉编译器,我知道我自己是什么类型,也知道自己在干什么,不需要给我报错
类型断言(Type Assertion): 可以用来手动指定一个值的类型(调用什么属性时,但不确定是什么类型)
语法:
    方式一: <类型>值
    方式二: 值 as 类型  tsx中只能用这种方式
*/
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

    function getLength2(x: number | string) {
        if ((<string>x).length) {
            return (x as string).length
        } else {
            return x.toString().length
        }
    }
    console.log(getLength2('abcd'), getLength2(1234))

2.as 语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

3.2 非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。

那么非空断言操作符到底有什么用呢?下面我们先来看一下非空断言操作符的一些使用场景。

1.忽略 undefined 和 null 类型(| 联合类型)

function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}

2.调用函数时忽略 undefined 类型

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

因为 ! 非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:

const a: number | undefined = undefined;
const b: number = a!;
console.log(b); 

以上 TS 代码会编译生成以下 ES5 代码:

"use strict";
const a = undefined;
const b = a;
console.log(b);

虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!; 语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,! 非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined

3.3 确定赋值断言

在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:

let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error

function initialize() {
  x = 10;
}

很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值

四、类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

4.1 in 关键字

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Admin | Employee;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

4.2 typeof 关键字

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型保护只支持两种形式:typeof v === "typename"typeof v !== typename"typename" 必须是 "number""string""boolean""symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

4.3 instanceof 关键字

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}

4.4 自定义类型保护的类型谓词

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

五、联合类型和类型别名

5.1 联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种
需求1: 定义一个一个函数得到一个数字或字符串值的字符串形式值

function toString2(x: number | string) : string {
  return x.toString()
}

联合类型通常与 nullundefined 一起使用:

const sayHello = (name: string | undefined) => {
  /* ... */
};

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给sayHello 函数。

sayHello("semlinker");
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

以上示例中的 12'click' 被称为字面量类型,用来约束取值只能是这几个值中的一个

注意,类型别名与字符串字面量类型都是使用 type 进行定义。 

5.2 可辨识联合

TypeScript 可辨识联合(Discriminated Unions)类型,也称类为代数数据型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。

这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

1.可辨识

可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:

enum CarTransmission {
  Automatic = 200,
  Manual = 300
}

interface Motorcycle {
  vType: "motorcycle"; // discriminant
  make: number; // year
}

interface Car {
  vType: "car"; // discriminant
  transmission: CarTransmission
}

interface Truck {
  vType: "truck"; // discriminant
  capacity: number; // in tons
}

在上述代码中,我们分别定义了 MotorcycleCarTruck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。

2.联合类型

基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:

type Vehicle = Motorcycle | Car | Truck;

现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆。

3.类型守卫

下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:

const EVALUATION_FACTOR = Math.PI; 

function evaluatePrice(vehicle: Vehicle) {
  return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);

对于以上代码,TypeScript 编译器将会提示以下错误信息:

Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:

function evaluatePrice(vehicle: Vehicle) {
  switch(vehicle.vType) {
    case "car":
      return vehicle.transmission * EVALUATION_FACTOR;
    case "truck":
      return vehicle.capacity * EVALUATION_FACTOR;
    case "motorcycle":
      return vehicle.make * EVALUATION_FACTOR;
  }
}

在以上代码中,我们使用 switchcase 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。

5.3 type 类型别名

类型别名用来给多个类型起个新名字成为一个新类型。

type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

 type(类型别名) 关键字用来定义一种类型,举个例子:

type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE'
let method: Methods
method = 'PUT' // OK
method = 'aaa' // error

在ts中,定义类型由两种方式:接口(interface)和类型别名(type alias)
interface只能定义对象类型,type声明的方式可以定义组合类型,交叉类型和原始类型
如果用type alias 声明的方式,会导致一些功能的缺失:

  1. interface方式可以实现接口的extends/implements,而type 不行
  2. interface可以实现接口的merge,但是type不行

六、交叉类型

在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
  x: 1,
  y: 1
}

在上面代码中我们先定义了 PartialPointX 类型,接着使用 & 运算符创建一个新的 Point 类型,表示一个含有 x 和 y 坐标的点,然后定义了一个 Point 类型的变量并初始化。

6.1 同名基础类型属性的合并

那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致,比如:

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string
}

type XY = X & Y;
type YX = Y & X;

let p: XY;
let q: YX;

在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 stringnumber 类型呢?比如下面的例子:

p = { c: 6, d: "d", e: "e" }; 

q = { c: "c", d: "d", e: "e" }; 

为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never 呢?这是因为混入后成员 c 的类型为 string & number,即成员 c 的类型既可以是 string 类型又可以是 number 类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never

6.2 同名非基础类型属性的合并

在上面示例中,刚好接口 X 和接口 Y 中内部成员 c 的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = {
  x: {
    d: true,
    e: 'semlinker',
    f: 666
  }
};

console.log('abc:', abc);

以上代码成功运行后,控制台会输出以下结果:

 由上图可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。

类型推断

类型推断: TS会在没有明确的指定类型的时候推测出一个类型
有下面2种情况: 1. 定义变量时赋值了, 推断为对应的类型. 2. 定义变量时没有赋值, 推断为any类型

/* 定义变量时赋值了, 推断为对应的类型 */
let b9 = 123 // number
// b9 = 'abc' // error

/* 定义变量时没有赋值, 推断为any类型 */
let b10  // any类型
b10 = 123
b10 = 'abc'

七、TypeScript 函数

7.1 TypeScript 函数与 JavaScript 函数的区别

TypeScriptJavaScript
含有类型无类型
箭头函数箭头函数(ES2015)
函数类型无函数类型
必填和可选参数所有参数都是可选的
默认参数默认参数
剩余参数剩余参数
函数重载无函数重载

7.2 箭头函数

1.常见语法

myBooks.forEach(() => console.log('reading'));

myBooks.forEach(title => console.log(title));

myBooks.forEach((title, idx, arr) =>
  console.log(idx + '-' + title);
);

myBooks.forEach((title, idx, arr) => {
  console.log(idx + '-' + title);
});

2.使用示例

// 未使用箭头函数
function Book() {
  let self = this;
  self.publishDate = 2016;
  setInterval(function () {
    console.log(self.publishDate);
  }, 1000);
}

// 使用箭头函数
function Book() {
  this.publishDate = 2016;
  setInterval(() => {
    console.log(this.publishDate);
  }, 1000);
}

7.3 参数类型和返回类型

function createUserId(name: string, id: number): string {
  return name + id;
}

7.4 函数类型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;

7.5 可选参数及默认参数

// 可选参数  ?
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默认参数
function createUserId(
  name: string = "semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误

7.6 剩余参数

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

7.7 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
  // type Combinable = string | number;
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。

方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:

class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
    return a + b;
  }
}

const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

八、TypeScript 数组

8.1 数组解构

let x: number; 
let y: number; 
let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;

8.2 数组展开运算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

8.3 数组遍历

let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
  console.log(i);
}

九、TypeScript 对象

9.1 对象解构

let person = {
  name: "Semlinker",
  gender: "Male",
};

let { name, gender } = person;

9.2 对象展开运算符

let person = {
  name: "Semlinker",
  gender: "Male",
  address: "Xiamen",
};

// 组装对象
let personWithAge = { ...person, age: 33 };

// 获取除了某些项外的其它项
let { name, ...rest } = person;

十、TypeScript 接口

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

10.1 对象的形状

/** 
 * TypeScript 的核心原则之一是对值所具有的结构(属性、参数等)进行类型检查。我们使用接口(Interfaces)来定义对象的类型。
 * 接口是对象的状态(属性)和行为(方法)的抽象(约束)
 * 接口:是一种类型,是一种规范,是一种能力,是一种约束
*/

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

let semlinker: Person = {
  name: "semlinker",
  age: 33,
};

10.2 可选 | 只读属性

/* 
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型
接口: 是对象的状态(属性)和行为(方法)的抽象(描述)
接口类型的对象
    多了或者少了属性是不允许的
    可选属性: ?
    只读属性: readonly
*/
interface Person {
  readonly name: string;
  age?: number;
}
// 定义一个对象,该对象的类型就是我定义的接口Person 
const person: Person = {
  name: 'tom',
  age: 20
}

只读属性用于限制只能在对象刚刚创建的时候修改其值
注意:如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。 

此外 TypeScript 还提供了 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候 

10.3 任意属性

有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。

interface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}

const p1: Person = { name: "semlinker" };
const p2: Person = { name: "lolo", age: 5 };
const p3: Person = { name: "kakuqo", sex: 1 }
console.log(p3);

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

 上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 numbernumber 不是 string 的子属性,所以报错了。

另外,在报错信息中可以看出,此时 { name: 'Tom', age: 25, gender: 'male' } 的类型被推断成了 { [x: string]: string | number; name: string; age: number; gender: string; },这是联合类型和接口的结合。

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

10.4 接口与类型别名的区别

1.Objects/Functions

接口和类型别名都可以用来描述对象的形状或函数签名:

接口

interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

类型别名

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

2.Other Types

与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组:

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

3.Extend

接口和类型别名都能够被扩展,但语法有所不同。此外,接口和类型别名不是互斥的。接口可以扩展类型别名,而反过来是不行的。

Interface extends interface

interface PartialPointX { x: number; }
interface Point extends PartialPointX { 
  y: number; 
}

Type alias extends type alias

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

Interface extends type alias

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

Type alias extends interface

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

4.Implements 类类型

类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型:

interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// A class can only implement an object type or 
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
  x = 1;
  y = 2;
}
// 类类型:类的类型,类的类型可以通过接口来实现
(() => {
    // 定义一个接口
    interface IFly {
        // 该方法没有任何的实现(方法中什么都没有)
        fly()
    }
    // 定义一个类,这个类的类型就是上面定义的接口(实际上也可以理解为,IFly接口约束了当前的这个 Person 类)
    class Person implements IFly {
        // 实现接口中的方法
        fly() {
            console.log('我会飞了,我是超人');
        }
    }
    // 实例化对象
    const person = new Person()
    person.fly()

    // 定义一个接口
    interface ISwim {
        swim()
    }
    // 定义一个类,这个类的类型就是 IFly 和 ISwim (当前这个类可以实现多个接口,一个类同时也可以被多个接口进行约束)
    class Person2 implements IFly, ISwim {
        // 实现接口中的方法
        fly() {
            console.log('我会飞了,我是超人');
        }
        swim() {
            console.log('我会游泳了');
        }
    }
    const person2 = new Person2()
    person2.fly()
    person2.swim()

    // 总结:类可以通过接口的方式,来定义当前这个类的类型
    // 类可以实现一个接口,类也可以实现多个接口,要注意:接口中的内容都要真正的实现

    // 定义一个接口,接口可以继承其他的多个接口
    interface IMyFlyAndSwim extends IFly, ISwim {
        say()
    }
    // 定义一个类,直接实现 IMyFlyAndSwim 这个接口
    class Person3 implements IMyFlyAndSwim {
        // 实现接口中的方法
        fly() {
            console.log('我会飞了,我是超人');
        }
        swim() {
            console.log('我会游泳了');
        }
        say() {
            console.log('我说');
        }
    }
    const person3 = new Person3()
    person3.say()


    // 总结:接口和接口之间叫继承(使用的是extends关键字),类和接口之间叫实现(使用的是implements关键字)
})()

 5.Declaration merging

与类型别名不同,接口可以定义多次,会被自动合并为单个接口。

interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

十一、TypeScript 类

类的概念

虽然 JavaScript 中有类的概念,但是可能大多数 JavaScript 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。

  • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
  • 对象(Object):类的实例,通过 new 生成
  • 面向对象(OOP)的三大特性:封装、继承、多态
  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
  • 存取器(getter & setter):用以改变属性的读取和赋值行为
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口

11.1 类的属性与方法

在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。

在 TypeScript 中,我们可以通过 Class 关键字来定义一个类:

class Greeter {
  // 静态属性
  static cname: string = "Greeter";
  // 成员属性
  greeting: string;

  // 构造函数 - 执行初始化操作(通过 new 生成新实例的时候,会自动调用构造函数)
  constructor(message: string) {
    this.greeting = message;
  }

  // 静态方法
  static getClassName() {
    return "Class name is Greeter";
  }

  // 成员方法
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下编译生成的 ES5 代码:

"use strict";
var Greeter = /** @class */ (function () {
    // 构造函数 - 执行初始化操作
    function Greeter(message) {
      this.greeting = message;
    }
    // 静态方法
    Greeter.getClassName = function () {
      return "Class name is Greeter";
    };
    // 成员方法
    Greeter.prototype.greet = function () {
      return "Hello, " + this.greeting;
    };
    // 静态属性
    Greeter.cname = "Greeter";
    return Greeter;
}());
var greeter = new Greeter("world");

ES7 中类的用法

ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。

实例属性

ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:

class Animal {
  name = 'Jack';

  constructor() {
    // ...
  }
}

let a = new Animal();
console.log(a.name); // Jack

11.2 ECMAScript 私有字段

在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:

class Person {
  #name: string;

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

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}

let semlinker = new Person("Semlinker");

semlinker.#name;
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  • 私有字段以 # 字符开头,有时我们称之为私有名称;
  • 每个私有字段名称都唯一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类之外访问,甚至不能被检测到

11.3 访问器

在 TypeScript 中,使用 getter 和 setter 可以改变属性的赋值和读取行为,也可以通过 gettersetter 方法来实现数据的封装和有效性校验,防止出现异常数据。

let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
  console.log(employee.fullName);
}

11.4 类的继承

继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。

继承是一种 is-a  关系:

在 TypeScript 中,我们可以通过 extends 关键字来实现继承:

class Animal {
  name: string;
  
  constructor(theName: string) {
    this.name = theName;
  }
  
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name); // 调用父类的构造函数
  }
  
  // 可以调用父类中的方法
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    // 调用父类中的move方法
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
sam.move();

// 总结:类和类之间如果要有继承关系,需要使用extends关键字
// 子类中可以调用父类中的构造函数,使用的是super关键字(包括调用父类中的实例方法,也可以使用super)
// 子类中可以重写父类的方法

11.5 抽象类

使用 abstract 关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法:

abstract class Person {
  constructor(public name: string){}

  abstract say(words: string) :void;
}

// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error

抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:

abstract class Person {
  constructor(public name: string){}

  // 抽象方法
  abstract say(words: string) :void;
}

class Developer extends Person {
  constructor(name: string) {
    super(name);
  }
  
  say(words: string): void {
    console.log(`${this.name} says ${words}`);
  }
}

const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!

11.6 类方法重载

在前面的章节,我们已经介绍了函数重载。对于类的方法来说,它也支持重载。比如,在以下示例中我们重载了 ProductService 类的 getProducts 成员方法:

class ProductService {
    getProducts(): void;
    getProducts(id: number): void;
    getProducts(id?: number) {
      if(typeof id === 'number') {
          console.log(`获取id为 ${id} 的产品信息`);
      } else {
          console.log(`获取所有的产品信息`);
      }  
    }
}

const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息 

11.7 多态

多态:父类型的引用指向了子类型的对象;不同类型的对象针对相同的方法,产生了不同的行为

// 多态:父类型的引用指向了子类型的对象;不同类型的对象针对相同的方法,产生了不同的行为
(() => {
    // 定义一个父类
    class Animal {
        // 定义一个属性
        name: string
        // 定义一个构造函数
        constructor(name: string) {
            // 更新属性值
            this.name = name
        }
        // 实例方法
        run(distance: number = 4) {
            console.log(`跑了${distance} 米这么远的距离`, this.name);
        }
    }
    // 定义一个子类
    class Dog extends Animal {
        // 构造函数
        constructor(name: string) {
            // 调用父类的构造函数,实现了子类中属性的初始化操作
            super(name)
        }
        // 实例方法,重写父类中的实例方法
        run(distance: number = 6) {
            console.log(`跑了${distance} 米这么远的距离`, this.name);
        }
    }
    // 定义一个子类
    class Pig extends Animal {
        // 构造函数
        constructor(name: string) {
            // 调用父类的构造函数,实现了子类中属性的初始化操作
            super(name)
        }
        // 实例方法,重写父类中的实例方法
        run(distance: number = 8) {
            console.log(`跑了${distance} 米这么远的距离`, this.name);
        }
    }

    // 实例化父类对象
    const ani: Animal = new Animal('动物')
    ani.run();
    // 实例化子类对象
    const dog: Dog = new Dog('大黄')
    dog.run()
    const pig: Pig = new Pig('八戒')
    pig.run()

    // 父类和子类的关系:父子关系;此时,父类类型创建子类的对象
    const dog1: Animal = new Dog('小黄')
    dog1.run()
    const pig1: Animal = new Pig('小猪')
    pig1.run()

    // 该函数需要的参数是 Animal 类型的
    function showRun(ani: Animal) {
        ani.run()
    }
    showRun(dog1)
    showRun(pig1)
})()

十二、TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

/**
 * 在定义函数或是类时,如果遇到类型不明确就可以使用泛型
 */
function fn<T>(a: T): T {
    return a;
}
// 可以直接调用具有泛型的函数
fn(18); // 不指定泛型,TS可以自动对类型进行推断(不一定都能推断出来)
fn<string>("hello"); // 指定泛型

12.1 泛型语法

对于刚接触 TypeScript 泛型的读者来说,首次看到 <T> 语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。

参考上面的图片,当我们调用 identity<Number>(1)Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

// 泛型:在定义函数、接口、类的时候不能预先确定要使用的数据的类型,而是在使用函数、接口、类的时候才能确定数据的类型
function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity(68, "Semlinker"));

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。

12.2 泛型接口

// 泛型接口:在定义接口时, 为接口中的属性或方法定义泛型类型,在使用接口时, 再指定具体的泛型类型
interface GenericIdentityFn<T> {
  name: T;
  (arg: T): T;
}


    // 需求:定义一个类,用来存储用户的相关信息(id,名字,年龄)
    // 通过一个类的实例对象调用相关的方法可以添加多个用户信息对象,调用getUserId方法可以根据id获取某个指定的用户信息对象

    // 定义一个泛型接口
    interface IBaseCRUD<T> {
        data: Array<T>
        add: (t: T) => T
        getUserId: (id: number) => T
    }
    // 定义一个用户信息的类
    class User {
        id?: number; //id主键
        name: string; //姓名
        age: number; //年龄

        constructor(name, age) {
            this.name = name
            this.age = age
        }
    }
    // 定义一个类,可以针对用户的信息对象进行增加及查询的操作  
    // CRUD--->create,read,update,delete 
    class UserCRUD implements IBaseCRUD<User>{
        data: Array<User> = []
        // 用来保存多个User类型的用户信息对象
        add(user: User): User {
            // 产生id
            user.id = Date.now() + Math.random()
            // 把用户信息对象添加到data数组中
            this.data.push(user)
            return user
        }
        // 根据id查询指定的用户信息对象
        getUserId(id: number): User {
            return this.data.find(user => user.id === id)
        }
    }
    // 实例化添加用户信息对象的类UserCRUD
    const userCRUD: UserCRUD = new UserCRUD()
    // 调用添加数据的方法
    userCRUD.add(new User("muzi", 20))
    userCRUD.add(new User("digbig", 21))
    const user = userCRUD.add(new User("muzidigbig", 22))
    console.log(userCRUD.data);
    // 根据id查询用户信息对象数据
    console.log(userCRUD.getUserId(user.id));

12.3 泛型类

// 泛型类:定义一个类,类中的属性值的类型是不确定的,方法中的参数及返回值的类型也是不确定的
class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

12.4 泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍 Partial 工具类型。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。

1.typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

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

const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person

function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

2.keyof

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

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

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

在 TypeScript 中支持两种索引签名,数字索引和字符串索引:

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number

3.in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

4.infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

5.extends

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

这时我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

6.Partial

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?

定义:

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

示例:

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};

const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript Enum",
});

在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:

{
   title?: string | undefined;
   description?: string | undefined;
}

十三、TypeScript 装饰器

13.1 装饰器是什么

  • 它是一个表达式
  • 该表达式被执行后,返回一个函数
  • 函数的入参分别为 target、name 和 descriptor
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象

13.2 装饰器的分类

  • 类装饰器(Class decorators)
  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:

命令行

tsc --target ES5 --experimentalDecorators

tsconfig.json

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

13.3 类装饰器

类装饰器声明:

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

类装饰器顾名思义,就是用来装饰类的。它接收一个参数:

  • target: TFunction - 被装饰的类

看完第一眼后,是不是感觉都不好了。没事,我们马上来个例子:

function Greeter(target: Function): void {
  target.prototype.greet = function (): void {
    console.log("Hello Semlinker!");
  };
}

@Greeter
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello Semlinker!';

上面的例子中,我们定义了 Greeter 类装饰器,同时我们使用了 @Greeter 语法糖,来使用装饰器。

友情提示:读者可以直接复制上面的代码,在 TypeScript Playground 中运行查看结果。

有的读者可能想问,例子中总是输出 Hello Semlinker! ,能自定义输出的问候语么 ?这个问题很好,答案是可以的。

具体实现如下:

function Greeter(greeting: string) {
  return function (target: Function) {
    target.prototype.greet = function (): void {
      console.log(greeting);
    };
  };
}

@Greeter("Hello TS!")
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';

13.4 属性装饰器

属性装饰器声明:

declare type PropertyDecorator = (target:Object, 
  propertyKey: string | symbol ) => void;

属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 被装饰类的属性名

趁热打铁,马上来个例子热热身:

function logProperty(target: any, key: string) {
  delete target[key];

  const backingField = "_" + key;

  Object.defineProperty(target, backingField, {
    writable: true,
    enumerable: true,
    configurable: true
  });

  // property getter
  const getter = function (this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };

  // property setter
  const setter = function (this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };

  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person { 
  @logProperty
  public name: string;

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

const p1 = new Person("semlinker");
p1.name = "kakuqo";

以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:

Set: name => semlinker
Set: name => kakuqo

13.5 方法装饰器

方法装饰器声明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, 	 	
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 属性描述符

废话不多说,直接上例子:

function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  let originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log("wrapped function: before invoking " + propertyKey);
    let result = originalMethod.apply(this, args);
    console.log("wrapped function: after invoking " + propertyKey);
    return result;
  };
}

class Task {
  @log
  runTask(arg: any): any {
    console.log("runTask invoked, args: " + arg);
    return "finished";
  }
}

let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result);

以上代码成功运行后,控制台会输出以下结果:

"wrapped function: before invoking runTask" 
"runTask invoked, args: learn ts" 
"wrapped function: after invoking runTask" 
"result: finished" 

下面我们来介绍一下参数装饰器。

13.6 参数装饰器

参数装饰器声明:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中参数的索引值
function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
	been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
	this.greeting = phrase; 
  }
}

以上代码成功运行后,控制台会输出以下结果:

"The parameter in position 0 at Greeter has been decorated" 

十四、TypeScript 4.0 新特性

TypeScript 4.0 带来了很多新的特性,这里我们只简单介绍其中的两个新特性。

14.1 构造函数的类属性推断

noImplicitAny 配置属性被启用之后,TypeScript 4.0 就可以使用控制流分析来确认类中的属性类型:

class Person {
  fullName; // (property) Person.fullName: string
  firstName; // (property) Person.firstName: string
  lastName; // (property) Person.lastName: string

  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName =   fullName.split(" ")[1];
  }  
}

然而对于以上的代码,如果在 TypeScript 4.0 以前的版本,比如在 3.9.2 版本下,编译器会提示以下错误信息:

class Person {
  // Member 'fullName' implicitly has an 'any' type.(7008)
  fullName; // Error
  firstName; // Error
  lastName; // Error

  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName =   fullName.split(" ")[1];
  }  
}

从构造函数推断类属性的类型,该特性给我们带来了便利。但在使用过程中,如果我们没法保证对成员属性都进行赋值,那么该属性可能会被认为是 undefined

class Person {
   fullName;  // (property) Person.fullName: string
   firstName; // (property) Person.firstName: string | undefined
   lastName; // (property) Person.lastName: string | undefined

   constructor(fullName: string) {
     this.fullName = fullName;
     if(Math.random()){
       this.firstName = fullName.split(" ")[0];
       this.lastName =   fullName.split(" ")[1];
     }
   }  
}

14.2 标记的元组元素

在以下的示例中,我们使用元组类型来声明剩余参数的类型:

function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}

addPerson("lolo", 5); // Person info: name: lolo, age: 5 

其实,对于上面的 addPerson 函数,我们也可以这样实现:

function addPerson(name: string, age: number) {
  console.log(`Person info: name: ${name}, age: ${age}`)
}

这两种方式看起来没有多大的区别,但对于第一种方式,我们没法设置第一个参数和第二个参数的名称。虽然这样对类型检查没有影响,但在元组位置上缺少标签,会使得它们难于使用。为了提高开发者使用元组的体验,TypeScript 4.0 支持为元组类型设置标签:

function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}

之后,当我们使用 addPerson 方法时,TypeScript 的智能提示就会变得更加友好。

// 未使用标签的智能提示
// addPerson(args_0: string, args_1: number): void
function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
} 

// 已使用标签的智能提示
// addPerson(name: string, age: number): void
function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
} 

十五、使用webpack打包TS

 15.1 下载依赖

npm add -D typescript
npm add -D webpack webpack-cli
npm add -D webpack-dev-server
npm add -D html-webpack-plugin clean-webpack-plugin
npm add -D ts-loader
npm add -D cross-env

 15.2 入口JS: src/main.ts

// import './01_helloworld'

document.write('Hello Webpack TS!')

15.3 index页面: public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webpack & TS</title>
</head>
<body>
  
</body>
</html>

15.4 build/webpack.config.js

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

const isProd = process.env.NODE_ENV === 'production' // 是否生产环境

function resolve (dir) {
  return path.resolve(__dirname, '..', dir)
}

module.exports = {
  mode: isProd ? 'production' : 'development',
  entry: {
    app: './src/main.ts'
  },

  output: {
    path: resolve('dist'),
    filename: '[name].[contenthash:8].js'
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        include: [resolve('src')]
      }
    ]
  },

  plugins: [
    new CleanWebpackPlugin({
    }),

    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],

  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },

  devtool: isProd ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',

  devServer: {
    host: 'localhost', // 主机名
    stats: 'errors-only', // 打包日志输出输出错误信息
    port: 8081,
    open: true
  },
}

15.5 配置打包命令

"dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"

15.6 运行与打包

npm run dev
npm run build

15.7 配置 tsconfig.json

如:十六

十六、编译上下文

16.1 tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径;
  • 用于配置 TypeScript 编译器;
  • 用于指定编译的文件。

16.2 tsconfig.json 重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

16.3 compilerOptions 选项

compilerOptions 支持很多选项,常见的有 baseUrltargetbaseUrlmoduleResolutionlib 等。

compilerOptions 每个选项的详细说明如下:

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}
复制代码

十七、TypeScript 开发辅助工具

17.1 TypeScript Playground

简介:TypeScript 官方提供的在线 TypeScript 运行环境,利用它你可以方便地学习 TypeScript 相关知识与不同版本的功能特性。

在线地址:www.typescriptlang.org/play/

除了 TypeScript 官方的 Playground 之外,你还可以选择其他的 Playground,比如 codepen.iostackblitzjsbin.com 等。

17.2 TypeScript UML Playground

简介:一款在线 TypeScript UML 工具,利用它你可以为指定的 TypeScript 代码生成 UML 类图。

在线地址:tsuml-demo.firebaseapp.com/

17.3 JSON TO TS

简介:一款 TypeScript 在线工具,利用它你可以为指定的 JSON 数据生成对应的 TypeScript 接口定义。

在线地址:www.jsontots.com/

除了使用 jsontots 在线工具之外,对于使用 VSCode IDE 的小伙们还可以安装 JSON to TS 扩展来快速完成 JSON to TS 的转换工作。

17.4 Schemats

简介:利用 Schemats,你可以基于(Postgres,MySQL)SQL 数据库中的 schema 自动生成 TypeScript 接口定义。

在线地址:github.com/SweetIQ/sch…

17.5 TypeScript AST Viewer

简介:一款 TypeScript AST 在线工具,利用它你可以查看指定 TypeScript 代码对应的 AST(Abstract Syntax Tree)抽象语法树。

在线地址:ts-ast-viewer.com/

对于了解过 AST 的小伙伴来说,对 astexplorer 这款在线工具应该不会陌生。该工具除了支持 JavaScript 之外,还支持 CSS、JSON、RegExp、GraphQL 和 Markdown 等格式的解析。

17.6 TypeDoc

简介:TypeDoc 用于将 TypeScript 源代码中的注释转换为 HTML 文档或 JSON 模型。它可灵活扩展,并支持多种配置。

在线地址:typedoc.org/

17.7 TypeScript ESLint

简介:使用 TypeScript ESLint 可以帮助我们规范代码质量,提高团队开发效率。

在线地址:typescript-eslint.io/

TypeScript ESLint 项目感兴趣且想在项目中应用的小伙伴,可以参考 “在Typescript项目中,如何优雅的使用ESLint和Prettier” 这篇文章。

能坚持看到这里的小伙伴都是 “真爱”,如果你还意犹未尽,那就来看看本人整理的 Github 上 1.8K+ 的开源项目:awesome-typescript

十八、参考资源

一份不可多得的 TS 学习指南(1.8W字)

《TypeScript三年高考五年模拟》 针对性训练

十九、在 TypeScript 中,import type和 import 的区别

在 TypeScript 中,import type 和 import 有以下区别:

  1. import type 仅仅引入类型信息,而不会引入实际的 JavaScript 对象。这意味着在编译后,import type 引入的代码不会出现在生成的 JavaScript 文件中。而 import 会引入实际的 JavaScript 对象
  2. import type 只能用于导入类型声明,例如接口、类型别名、枚举等。而 import 可以用于导入值、函数、类等任何东西。
  3. 在使用 import type 时,无法使用导入的类型作为值进行实例化或传递给函数。这意味着,你无法使用 import type 导入的类型来创建一个对象或者传递给一个函数作为参数,因为这些类型只存在于编译时。
// foo.ts
export interface Foo {
  bar: string;
}

// index.ts
import { Foo } from './foo'; // 这里导入了实际的 Foo 接口对象
import type { Foo as FooType } from './foo'; // 这里导入了 Foo 接口的类型信息

const foo: Foo = { bar: 'baz' }; // 正常使用 Foo 接口对象
const fooType: FooType = { bar: 'baz' }; // 无法使用 Foo 接口的类型信息创建一个对象,TypeScript3.8以前的版本这里会报错。3.8之后的版本就不会报错了

因此,当你只需要类型信息时,应该使用 import type。这样可以避免引入多余的代码,并提高编译性能。而当你需要实际的 JavaScript 对象时,则需要使用 import

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐