乐趣区

关于前端:18W字一份不可多得的-TS-学习指南

阿宝哥第一次应用 TypeScript 是在 Angular 2.x 我的项目中,那时候 TypeScript 还没有进入公众的视线。然而当初学习 TypeScript 的小伙伴越来越多了,本文阿宝哥将从 16 个方面动手,带你一步步学习 TypeScript,感兴趣的小伙伴不要错过。

一、TypeScript 是什么

TypeScript 是一种由微软开发的自在和开源的编程语言。它是 JavaScript 的一个超集,而且实质上向这个语言增加了可选的动态类型和基于类的面向对象编程。

TypeScript 提供最新的和一直倒退的 JavaScript 个性,包含那些来自 2015 年的 ECMAScript 和将来的提案中的个性,比方异步性能和 Decorators,以帮忙建设强壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:

1.1 TypeScript 与 JavaScript 的区别

TypeScript JavaScript
JavaScript 的超集用于解决大型项目的代码复杂性 一种脚本语言,用于创立动静网页
能够在编译期间发现并纠正错误 作为一种解释型语言,只能在运行时发现错误
强类型,反对动态和动静类型 弱类型,没有动态类型选项
最终被编译成 JavaScript 代码,使浏览器能够了解 能够间接在浏览器中应用
反对模块、泛型和接口 不反对模块,泛型或接口
社区的反对仍在增长,而且还不是很大 大量的社区反对以及大量文档和解决问题的反对

1.2 获取 TypeScript

命令行的 TypeScript 编译器能够应用 npm 包管理器来装置。

1. 装置 TypeScript
$ npm install -g typescript
2. 验证 TypeScript
$ tsc -v 
# Version 4.0.2
3. 编译 TypeScript 文件
$ tsc helloworld.ts
# helloworld.ts => helloworld.js

当然,对刚入门 TypeScript 的小伙伴来说,也能够不必装置 typescript,而是间接应用线上的 TypeScript Playground 来学习新的语法或新个性。通过配置 TS Config 的 Target,能够设置不同的编译指标,从而编译生成不同的指标代码。

下图示例中所设置的编译指标是 ES5:

(图片起源:https://www.typescriptlang.or…)

1.3 典型 TypeScript 工作流程

如你所见,在上图中蕴含 3 个 ts 文件:a.ts、b.ts 和 c.ts。这些文件将被 TypeScript 编译器,依据配置的编译选项编译成 3 个 js 文件,即 a.js、b.js 和 c.js。对于大多数应用 TypeScript 开发的 Web 我的项目,咱们还会对编译生成的 js 文件进行打包解决,而后在进行部署。

1.4 TypeScript 初体验

新建一个 hello.ts 文件,并输出以下内容:

function greet(person: string) {return 'Hello,' + person;}

console.log(greet("TypeScript"));

而后执行 tsc hello.ts 命令,之后会生成一个编译好的文件 hello.js

"use strict";
function greet(person) {return 'Hello,' + person;}
console.log(greet("TypeScript"));

察看以上编译后的输入后果,咱们发现 person 参数的类型信息在编译后被擦除了。TypeScript 只会在编译阶段对类型进行动态查看,如果发现有谬误,编译时就会报错。而在运行时,编译生成的 JS 与一般的 JavaScript 文件一样,并不会进行类型查看。

二、TypeScript 根底类型

2.1 Boolean 类型

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

2.2 Number 类型

let count: number = 10;
// ES5:var count = 10;

2.3 String 类型

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

2.4 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];

let list: Array<number> = [1, 2, 3]; // Array<number> 泛型语法
// ES5:var list = [1,2,3];

2.6 Enum 类型

应用枚举咱们能够定义一些带名字的常量。应用枚举能够清晰地表白用意或创立一组有区别的用例。TypeScript 反对数字的和基于字符串的枚举。

1. 数字枚举
enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

默认状况下,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 的初始值,比方:

enum Direction {
  NORTH = 3,
  SOUTH,
  EAST,
  WEST,
}
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 */;
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

2.7 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.8 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.9 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]'.

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 类型的变量没有什么作用,因为它的值只能为 undefinednull

let unusable: void = undefined;

2.11 Null 和 Undefined 类型

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

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

默认状况下 nullundefined 是所有类型的子类型。就是说你能够把 nullundefined 赋值给 number 类型的变量。然而,如果你指定了--strictNullChecks 标记,nullundefined 只能赋值给 void 和它们各自的类型。

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 类型示意的是那些永不存在的值的类型。例如,never 类型是那些总是会抛出异样或基本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

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

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

在 TypeScript 中,能够利用 never 类型的个性来实现全面性查看,具体示例如下:

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 断言

3.1 类型断言

有时候你会遇到这样的状况,你会比 TypeScript 更理解某个值的详细信息。通常这会产生在你分明地晓得一个实体具备比它现有类型更确切的类型。

通过类型断言这种形式能够通知编译器,“置信我,我晓得本人在干什么”。类型断言好比其余语言里的类型转换,然而不进行非凡的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种模式:

1.“尖括号”语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
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 = Employee | Admin;

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 联结类型

联结类型通常与 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' 被称为字面量类型,用来束缚取值只能是某几个值中的一个。

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 Message = string | string[];

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

六、穿插类型

在 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);

以上代码胜利运行后,控制台会输入以下后果:

由上图可知,在混入多个类型时,若存在雷同的成员,且成员类型为非根本数据类型,那么是能够胜利合并。

七、TypeScript 函数

7.1 TypeScript 函数与 JavaScript 函数的区别

TypeScript JavaScript
含有类型 无类型
箭头函数 箭头函数(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 对象的形态

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

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

10.2 可选 | 只读属性

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

只读属性用于限度只能在对象刚刚创立的时候批改其值。此外 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 = {name: "semlinker"};
const p2 = {name: "lolo", age: 5};
const p3 = {name: "kakuqo", sex: 1}

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;
}
5.Declaration merging

与类型别名不同,接口能够定义屡次,会被主动合并为单个接口。

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

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

十一、TypeScript 类

11.1 类的属性与办法

在面向对象语言中,类是一种面向对象计算机编程语言的结构,是创建对象的蓝图,形容了所创立的对象独特的属性和办法。

在 TypeScript 中,咱们能够通过 Class 关键字来定义一个类:

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

  // 构造函数 - 执行初始化操作
  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");

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 中,咱们能够通过 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...");
    super.move(distanceInMeters);
  }
}

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

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(); // 获取所有的产品信息 

十二、TypeScript 泛型

软件工程中,咱们不仅要创立统一的定义良好的 API,同时也要思考可重用性。组件不仅可能反对以后的数据类型,同时也能反对将来的数据类型,这在创立大型零碎时为你提供了非常灵便的性能。

在像 C# 和 Java 这样的语言中,能够应用泛型来创立可重用的组件,一个组件能够反对多种类型的数据。这样用户就能够以本人的数据类型来应用组件。

设计泛型的要害目标是在成员之间提供有意义的束缚,这些成员能够是:类的实例成员、类的办法、函数参数和函数返回值。

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

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<Number, string>(68, "Semlinker"));

除了为类型变量显式设定值之外,一种更常见的做法是使编译器主动抉择这些类型,从而使代码更简洁。咱们能够齐全省略尖括号,比方:

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> {(arg: T): T;
}

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.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.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 LogOutput(tarage: Function, key: string, descriptor: any) {
  let originalMethod = descriptor.value;
  let newMethod = function(...args: any[]): any {let result: any = originalMethod.apply(this, args);
    if(!this.loggedOutput) {this.loggedOutput = new Array<any>();
    }
    this.loggedOutput.push({
      method: key,
      parameters: args,
      output: result,
      timestamp: new Date()});
    return result;
  };
  descriptor.value = newMethod;
}

class Calculator {
  @LogOutput
  double (num: number): number {return num * 2;}
}

let calc = new Calculator();
calc.double(11);
// console ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput); 

上面咱们来介绍一下参数装璜器。

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;}
}

// console output: 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]}`);
} 

十五、编译上下文

15.1 tsconfig.json 的作用

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

15.2 tsconfig.json 重要字段

  • files – 设置要编译的文件的名称;
  • include – 设置须要进行编译的文件,反对门路模式匹配;
  • exclude – 设置无需进行编译的文件,反对门路模式匹配;
  • compilerOptions – 设置与编译流程相干的选项。

15.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 开发辅助工具

16.1 TypeScript Playground

简介:TypeScript 官网提供的在线 TypeScript 运行环境,利用它你能够不便地学习 TypeScript 相干常识与不同版本的性能个性。

在线地址:https://www.typescriptlang.or…

除了 TypeScript 官网的 Playground 之外,你还能够抉择其余的 Playground,比方 codepen.io、stackblitz 或 jsbin.com 等。

16.2 TypeScript UML Playground

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

在线地址:https://tsuml-demo.firebaseap…

16.3 JSON TO TS

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

在线地址:http://www.jsontots.com/

除了应用 jsontots 在线工具之外,对于应用 VSCode IDE 的小伙们还能够装置 JSON to TS 扩大来疾速实现 JSON to TS 的转换工作。

16.4 Schemats

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

在线地址:https://github.com/SweetIQ/sc…

16.5 TypeScript AST Viewer

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

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

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

16.6 TypeDoc

简介:TypeDoc 用于将 TypeScript 源代码中的正文转换为 HTML 文档或 JSON 模型。它可灵便扩大,并反对多种配置。

在线地址:https://typedoc.org/

16.7 TypeScript ESLint

简介:应用 TypeScript ESLint 能够帮忙咱们标准代码品质,进步团队开发效率。

在线地址:https://typescript-eslint.io/

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

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

https://github.com/semlinker/…

十七、参考资源

  • mariusschulz – the-unknown-type-in-typescript
  • 深刻了解 TypeScript – 编译上下文
  • TypeScript 4.0
  • TypeScript Quickly

十八、举荐浏览

  • 了不起的 Deno 入门篇
  • 了不起的 Deno 实战教程
  • 你不晓得的 Blob
  • 你不晓得的 WeakMap
退出移动版