了不得的 TypeScript 入門教程(1.2W字)

想學習 TypeScript 的小夥伴看過來,本文將帶你一步步學習 TypeScript 入門相關的十四個知識點,詳細的內容大綱請看下圖:javascript

typescript-quickstart-directory.png

1、TypeScript 是什麼

TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,並且本質上向這個語言添加了可選的靜態類型和基於類的面向對象編程。html

TypeScript 提供最新的和不斷髮展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和將來的提案中的特性,好比異步功能和 Decorators,以幫助創建健壯的組件。下圖顯示了 TypeScript 與 ES五、ES2015 和 ES2016 之間的關係:java

typescript-scope-new.png

1.1 TypeScript 與 JavaScript 的區別

TypeScript JavaScript
JavaScript 的超集用於解決大型項目的代碼複雜性 一種腳本語言,用於建立動態網頁。
能夠在編譯期間發現並糾正錯誤 做爲一種解釋型語言,只能在運行時發現錯誤
強類型,支持靜態和動態類型 弱類型,沒有靜態類型選項
最終被編譯成 JavaScript 代碼,使瀏覽器能夠理解 能夠直接在瀏覽器中使用
支持模塊、泛型和接口 不支持模塊,泛型或接口
支持 ES3,ES4,ES5 和 ES6 等 不支持編譯其餘 ES3,ES4,ES5 或 ES6 功能
社區的支持仍在增加,並且還不是很大 大量的社區支持以及大量文檔和解決問題的支持

1.2 獲取 TypeScript

命令行的 TypeScript 編譯器可使用 Node.js 包來安裝。node

1.安裝 TypeScriptreact

$ npm install -g typescript

2.編譯 TypeScript 文件git

$ tsc helloworld.ts
# helloworld.ts => helloworld.js

固然,對於剛入門 TypeScript 的小夥伴,也能夠不用安裝 typescript,而是直接使用線上的 TypeScript Playground 來學習新的語法或新特性。github

TypeScript Playground: https://www.typescriptlang.or...

2、TypeScript 基礎類型

2.1 Boolean 類型

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

2.2 Number 類型

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

String 類型

let name: string = "Semliker";
// ES5:var name = 'Semlinker';

2.4 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.5 Enum 類型

使用枚舉咱們能夠定義一些帶名字的常量。 使用枚舉能夠清晰地表達意圖或建立一組有區別的用例。 TypeScript 支持數字的和基於字符串的枚舉。typescript

1.數字枚舉shell

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

let dir: Direction = Direction.NORTH;

默認狀況下,NORTH 的初始值爲 0,其他的成員會從 1 開始自動增加。換句話說,Direction.SOUTH 的值爲 1,Direction.EAST 的值爲 2,Direction.WEST 的值爲 3。上面的枚舉示例代碼通過編譯後會生成如下代碼:express

"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 = {}));

3.異構枚舉

異構枚舉的成員值是數字和字符串的混合:

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.6 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.7 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.8 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.9 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.10 Null 和 Undefined 類型

TypeScript 裏,undefinednull 二者有各自的類型分別爲 undefinednull

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

默認狀況下 nullundefined 是全部類型的子類型。 就是說你能夠把 nullundefined 賦值給 number 類型的變量。然而,若是你指定了--strictNullChecks 標記,nullundefined 只能賦值給 void 和它們各自的類型。

2.11 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 斷言

有時候你會遇到這樣的狀況,你會比 TypeScript 更瞭解某個值的詳細信息。一般這會發生在你清楚地知道一個實體具備比它現有類型更確切的類型。

經過類型斷言這種方式能夠告訴編譯器,「相信我,我知道本身在幹什麼」。類型斷言比如其餘語言裏的類型轉換,可是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起做用。

類型斷言有兩種形式:

3.1 「尖括號」 語法

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

3.2 as 語法

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

4、類型守衛

A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— 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、聯合類型和類型別名

5.1 聯合類型

聯合類型一般與 nullundefined 一塊兒使用:

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

例如,這裏 name 的類型是 string | undefined 意味着能夠將 stringundefined 的值傳遞給sayHello 函數。

sayHello("Semlinker");
sayHello(undefined);

經過這個示例,你能夠憑直覺知道類型 A 和類型 B 聯合後的類型是同時接受 A 和 B 值的類型。

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 交叉類型是將多個類型合併爲一個類型。 這讓咱們能夠把現有的多種類型疊加到一塊兒成爲一種類型,它包含了所需的全部類型的特性。

interface IPerson {
  id: string;
  age: number;
}

interface IWorker {
  companyId: string;
}

type IStaff = IPerson & IWorker;

const staff: IStaff = {
  id: 'E1006',
  age: 33,
  companyId: 'EFT'
};

console.dir(staff)

在上面示例中,咱們首先爲 IPerson 和 IWorker 類型定義了不一樣的成員,而後經過 & 運算符定義了 IStaff 交叉類型,因此該類型同時擁有 IPerson 和 IWorker 這兩種類型的成員。

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) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}

在以上代碼中,咱們爲 add 函數提供了多個函數類型定義,從而實現函數的重載。以後,可惡的錯誤消息又消失了,由於這時 result 變量的類型是 string 類型。在 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!

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 訪問器

在 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.3 類的繼承

繼承 (Inheritance) 是一種聯結類與類的層次模型。指的是一個類(稱爲子類、子接口)繼承另外的一個類(稱爲父類、父接口)的功能,並能夠增長它本身的新功能的能力,繼承是類與類或者接口與接口之間最多見的關係。

繼承是一種 is-a 關係:

ts-is-a.jpeg

在 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.4 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);
  • 私有字段不能在包含的類以外訪問,甚至不能被檢測到。

12、TypeScript 泛型

軟件工程中,咱們不只要建立一致的定義良好的 API,同時也要考慮可重用性。 組件不只可以支持當前的數據類型,同時也能支持將來的數據類型,這在建立大型系統時爲你提供了十分靈活的功能。

在像 C# 和 Java 這樣的語言中,可使用泛型來建立可重用的組件,一個組件能夠支持多種類型的數據。 這樣用戶就能夠以本身的數據類型來使用組件。

設計泛型的關鍵目的是在成員之間提供有意義的約束,這些成員能夠是:類的實例成員、類的方法、函數參數和函數返回值。

泛型(Generics)是容許同一個函數接受不一樣類型參數的一種模板。相比於使用 any 類型,使用泛型來建立可複用的組件要更好,由於泛型會保留參數類型。

12.1 泛型接口

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

12.2 泛型類

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.3 泛型變量

對剛接觸 TypeScript 泛型的小夥伴來講,看到 T 和 E,還有 K 和 V 這些泛型變量時,估計會一臉懵逼。其實這些大寫字母並無什麼本質的區別,只不過是一個約定好的規範而已。也就是說使用大寫字母 A-Z 定義的類型變量都屬於泛型,把 T 換成 A,也是同樣的。下面咱們介紹一下一些常見泛型變量表明的意思:

  • T(Type):表示一個 TypeScript 類型
  • K(Key):表示對象中的鍵類型
  • V(Value):表示對象中的值類型
  • E(Element):表示元素類型

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

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 ILengthwise {
  length: number;
}

function loggingIdentity<T extends ILengthwise>(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: "organize desk",
  description: "clear clutter",
};

const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});

在上面的 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)

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、編譯上下文

14.1 tsconfig.json 的做用

  • 用於標識 TypeScript 項目的根路徑;
  • 用於配置 TypeScript 編譯器;
  • 用於指定編譯的文件。

14.2 tsconfig.json 重要字段

  • files - 設置要編譯的文件的名稱;
  • include - 設置須要進行編譯的文件,支持路徑模式匹配;
  • exclude - 設置無需進行編譯的文件,支持路徑模式匹配;
  • compilerOptions - 設置與編譯流程相關的選項。

14.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          // 爲裝飾器提供元數據的支持
  }
}

看到這裏的讀者都是「真愛」,若是你還意猶未盡,那就來看看本人整理的 Github 上 1.5K+ 的開源項目:awesome-typescript

https://github.com/semlinker/...

十5、參考資源

十6、推薦閱讀

相關文章
相關標籤/搜索