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

阿宝哥第一次使用 TypeScript 是在 Angular 2.x 项目中,那时候 TypeScript 尚未进入大众的视野。然而如今学习 TypeScript 的小伙伴愈来愈多了,本文阿宝哥将从 16 个方面入手,带你一步步学习 TypeScript,感兴趣的小伙伴不要错过。javascript

1、TypeScript 是什么

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

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和将来的提案中的特性,好比异步功能和 Decorators,以帮助创建健壮的组件。下图显示了 TypeScript 与 ES五、ES2015 和 ES2016 之间的关系:java

1.1 TypeScript 与 JavaScript 的区别

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

1.2 获取 TypeScript

命令行的 TypeScript 编译器可使用 npm 包管理器来安装。node

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,能够设置不一样的编译目标,从而编译生成不一样的目标代码。react

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

(图片来源:www.typescriptlang.org/play)github

1.3 典型 TypeScript 工做流程

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

1.4 TypeScript 初体验

新建一个 hello.ts 文件,并输入如下内容:shell

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 文件同样,并不会进行类型检查。

2、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 类型的变量没有什么做用,由于在严格模式下,它的值只能为 undefined

let unusable: void = undefined;
复制代码

2.11 Null 和 Undefined 类型

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

let u: undefined = undefined;
let n: null = 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 类型表示的是那些永不存在的值的类型。 例如,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 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

3、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、类型守卫

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

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、联合类型和类型别名

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) => {
  // ...
};
复制代码

6、交叉类型

在 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);
复制代码

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

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

7、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 成员方法来讲,咱们只定义了四个重载方法。

8、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);
}
复制代码

9、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;
复制代码

10、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 };
复制代码

11、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(); // 获取全部的产品信息 
复制代码

12、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;
}
复制代码

十3、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" 
复制代码

十4、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]}`);
} 
复制代码

十5、编译上下文

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          // 为装饰器提供元数据的支持
  }
}
复制代码

十6、TypeScript 开发辅助工具

16.1 TypeScript Playground

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

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

除了 TypeScript 官方的 Playground 以外,你还能够选择其余的 Playground,好比 codepen.iostackblitzjsbin.com 等。

16.2 TypeScript UML Playground

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

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

16.3 JSON TO TS

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

在线地址:www.jsontots.com/

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

16.4 Schemats

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

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

16.5 TypeScript AST Viewer

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

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

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

16.6 TypeDoc

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

在线地址:typedoc.org/

16.7 TypeScript ESLint

简介:使用 TypeScript ESLint 能够帮助咱们规范代码质量,提升团队开发效率。

在线地址:typescript-eslint.io/

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

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

十7、参考资源

相关文章
相关标签/搜索