「1-8W 字」不可多得的 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 的區別

PY6WLc

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.org/play)

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 類的實例的類型,它由以下兩個接口來定義:

// 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;
}
// 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 函數的區別

veMh6O

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 修飾符聲明的屬性)不同,私有字段要牢記以下規則:

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 之外,以下是常見泛型變量代表的意思:

其實並不是只能定義一個類型變量,我們可以引入希望定義的任何數量的類型變量。比如我們引入一個新的類型變量 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 操作符可以用來一個對象中的所有 key 值:

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 裝飾器是什麼

13.2 裝飾器的分類

需要注意的是,若要啓用實驗性的裝飾器特性,你必須在命令行或 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;

類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:

看完第一眼後,是不是感覺都不好了。沒事,我們馬上來個例子:

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;

屬性裝飾器顧名思義,用來裝飾類的屬性。它接收兩個參數:

趁熱打鐵,馬上來個例子熱熱身:

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;

方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數:

廢話不多說,直接上例子:

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

參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:

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 的作用

15.2 tsconfig.json 重要字段

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.org/play/

除了 TypeScript 官方的 Playground 之外,你還可以選擇其他的 Playground,比如 codepen.io、stackblitz 或 jsbin.com 等。

16.2 TypeScript UML Playground

簡介:一款在線 TypeScript UML 工具,利用它你可以爲指定的 TypeScript 代碼生成 UML 類圖。

在線地址:https://tsuml-demo.firebaseapp.com/

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/schemats

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/awesome-typescript

十七、參考資源

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PCQvZt78AM11EI2bJ_S6ew