天天看點

如何在 TypeScript 中使用類

如何在 TypeScript 中使用類

英文 | https://www.digitalocean.com/community/tutorials/how-to-use-classes-in-typescript

翻譯 | 楊小愛

介紹

類是面向對象程式設計 (OOP) 語言中用于描述對象的資料結構的常見對象。這些對象可能包含初始狀态并實作綁定到該特定對象執行個體的行為。 

2015 年,ECMAScript 6 為 JavaScript 引入了一種新文法,以建立内部使用的原型文法類。TypeScript 完全支援該文法,并在其之上添加了一些特性,如可見性、抽象類、泛型類、箭頭函數方法等。

本教程将介紹用于建立類的文法、可用的不同功能以及在編譯時類型檢查期間如何在 TypeScript 中處理類。它将引導我們完成具有不同代碼示例的示例,我們可以在自己的 TypeScript 環境中遵循這些示例。

提前準備工作

要完成本教程,你需要做好以下工作:

一個環境,您可以在其中執行 TypeScript 程式以跟随示例。要在本地計算機上進行設定,您将需要準備以下内容:

為了運作處理 TypeScript 相關包的開發環境,同時,安裝了 Node 和 npm(或 yarn)。本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 進行了測試。要在 macOS 或 Ubuntu 18.04 上安裝,請按照如何在 macOS 上安裝 Node.js 和建立本地開發環境或如何在 Ubuntu 18.04 上安裝 Node.js 的使用 PPA 安裝部分中的步驟進行操作。如果您使用的是适用于 Linux 的 Windows 子系統 (WSL),這也适用。

此外,我們需要在機器上安裝 TypeScript 編譯器 (tsc)。為此,請參閱官方 TypeScript 網站,https://www.typescriptlang.org/download

如果你不想在本地機器上建立 TypeScript 環境,你可以使用官方的 TypeScript Playground 來完成。

您将需要足夠的 JavaScript 知識,尤其是 ES6+ 文法,例如解構、rest 運算符和導入/導出。

本教程将參考支援 TypeScript 并顯示内聯錯誤的文本編輯器的各個方面。這不是使用 TypeScript 所必需的,但确實可以更多地利用 TypeScript 功能。為了獲得這些好處,您可以使用像 Visual Studio Code 這樣的文本編輯器,它完全支援開箱即用的 TypeScript。你也可以在 TypeScript Playground(位址:https://www.typescriptlang.org/play) 中嘗試這些好處。

本教程中顯示的所有示例都是使用 TypeScript 4.3.2 版建立的。

在 TypeScript 中建立類

在本節中,我們将學習用在 TypeScript 中建立類的文法示例。雖然,我們将介紹使用 TypeScript 建立類的一些基本方面,但文法與使用 JavaScript 建立類的文法基本相同。是以,本教程将重點介紹 TypeScript 中可用的一些顯着特性。

我們可以使用 class 關鍵字建立類聲明,後跟類名,然後是 {} 對塊,如以下代碼所示:​

class Person {
}      

該片段建立了一個名為 Person 的新類。然後,我們可以使用 new 關鍵字後跟的類的名稱,接着是一個空參數清單(可以省略)來建立 Person 類的新執行個體,如以下突出顯示的代碼所示:

class Person {


}


const personInstance = new Person();      

我們可以将類本身視為建立具有給定形狀的對象的藍圖,而執行個體是從該藍圖建立的對象本身。

在使用類時,大多數時候您需要建立一個constructor函數。constructor函數是每次建立類的新執行個體時運作的方法。這可用于初始化類中的值。

為Person 類引入一個constructor函數:​

class Person {
  constructor() {
    console.log("Constructor called");
  }
}


const personInstance = new Person();      

此constructor函數将在建立 personInstance 時将調用的構造函數記錄到控制台。

constructor函數在接受參數的方式上類似于普通函數。當我們建立類的新執行個體時,這些參數将傳遞給構造函數。目前,我們沒有将任何參數傳遞給constructor函數,如建立類的執行個體時的空參數清單 () 所示。

接下來,引入一個string(字元串)類型名稱的新參數:​

class Person {
  constructor(name: string) {
    console.log(`Constructor called with name=${name}`);
  }
}


const personInstance = new Person("Jane");      

在突出顯示的代碼中,我們向類構造函數添加了一個名為字元串類型名稱的參數。然後,在建立 Person 類的新執行個體時,我們還設定了該參數的值,在本例中“Jane”為字元串。最後,我們更改了 console.log 以将參數列印到螢幕上。

如果我們要運作此代碼,我們将在終端中收到以下輸出:

Output Constructor called with name=Jane      

構造函數中的參數在這裡不是可選的。這意味着在執行個體化類時,必須将 name 參數傳遞給構造函數。如果不将 name 參數傳遞給構造函數,如下例所示:

const unknownPerson = new Person;      

TypeScript 編譯器将報出錯誤 2554:

OutputExpected 1 arguments, but got 0. (2554)
filename.ts(4, 15): An argument for 'name' was not provided.      

現在我們已經在 TypeScript 中聲明了一個類,我們将繼續通過添加屬性來操作這些類。

添加類屬性

類最有用的地方之一是,它們能夠儲存從類建立的每個執行個體的内部資料。這是使用屬性完成的。

TypeScript 有一些安全檢查可以将此過程與 JavaScript 類區分開來,包括要求初始化屬性以避免它們未定義。在本節中,我們将向你的類添加新屬性以說明這些安全檢查。

使用 TypeScript,我們通常必須首先在類的主體中聲明屬性并為其指定類型。例如,給 name 屬性添加到 Person 類:

class Person {
  name: string;


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

在此示例中,除了在構造函數中設定屬性外,還使用類型字元串聲明屬性名稱。

注意:在 TypeScript 中,我們還可以聲明類中屬性的可見性以确定可以通路資料的位置。在 name:string 聲明中,沒有聲明可見性,這意味着該屬性使用在任何地方都可以通路的預設公共狀态。如果你想顯示控制可見性,我們可以将其與屬性一起聲明。這将在本教程後面更深入地介紹。

我們還可以為屬性提供預設值。例如,添加一個名為 instantiatedAt 的新屬性,該屬性将設定為執行個體化類執行個體的時間:

class Person {
  name: string;
  instantiatedAt = new Date();


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

這使用 Date 對象來設定建立執行個體的初始日期。這段代碼之是以有效,是因為在調用類構造函數時會執行預設值的代碼,這相當于在構造函數上設定值,如下所示:

class Person {
  name: string;
  instantiatedAt: Date;


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

通過在類的主體中聲明預設值,我們無需在構造函數中設定該值。

注意,如果我們為類中的屬性設定類型,則還必須将該屬性初始化為該類型的值。為了說明這一點,聲明一個類屬性但不為其提供初始化器,如下面的代碼所示:

class Person {
  name: string;
  instantiatedAt: Date;


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

instantiatedAt 配置設定了一個 Date 類型,是以,必須始終是一個 Date 對象。但是由于沒有初始化,是以當類被執行個體化時,屬性變得未定義。是以,TypeScript 編譯器将顯示錯誤 2564:

Output Property 'instantiatedAt' has no initializer and is not definitely assigned in the constructor. (2564)      

這是一項額外的 TypeScript 安全檢查,以確定在類執行個體化時存在正确的屬性。

TypeScript 還有一個快捷方式,用于編寫與傳遞給構造函數的參數同名的屬性。此快捷方式稱為參數屬性。

在前面的示例中,我們将 name 屬性設定為傳遞給類構造函數的 name 參數的值。如果我們在類中添加更多字段,這可能會變得令人厭煩。例如,在Person 類中添加一個名為 age 類型的新字段,并将其添加到構造函數中:

class Person {
  name: string;
  age: number;
  instantiatedAt = new Date();


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

雖然這可行,但 TypeScript 可以使用參數屬性或在構造函數的參數中設定的屬性來減少此類樣闆代碼:

class Person {
  instantiatedAt = new Date();


  constructor(
    public name: string,
    public age: number
) {}
}      

在此代碼段中,我們從類主體中删除了 name 和 age 屬性聲明,并将它們移動到構造函數的參數清單中。當我們這樣做時,我們是在告訴 TypeScript 這些構造函數參數也是該類的屬性。這樣,就不需要像以前那樣将類的屬性設定為構造函數中接收到的參數的值。

注意:可見性修飾符 public 已在代碼中明确說明。設定參數屬性時必須包含此修飾符,并且不會自動預設為公共可見性。

如果你檢視由 TypeScript Compiler 發出的已編譯 JavaScript,此代碼将編譯為以下 JavaScript 代碼:

"use strict";
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.instantiatedAt = new Date();
  }
}      

這與原始示例編譯成的 JavaScript 代碼相同。

現在,我們已經嘗試在 TypeScript 類上設定屬性,可以繼續将類擴充為具有類繼承的新類。

TypeScript 中的類繼承

TypeScript 提供了 JavaScript 類繼承的全部功能,主要增加了兩個功能:接口和抽象類。

接口是一種描述和強制類或對象形狀的結構,例如為更複雜的資料片段提供類型檢查。可以在類中實作接口以確定它具有特定的公共形狀。抽象類是作為其他類的基礎的類,但它們本身不能被執行個體化。這兩者都是通過類繼承實作的。

在本節中,我們将通過一些示例了解如何使用接口和抽象類來建構和建立類的類型檢查。

實作接口

接口可用于指定該接口的所有實作必須具備的一組行為。接口是通過使用 interface 關鍵字後跟接口名稱,然後是接口主體來建立的。例如,建立一個 Logger 接口,該接口可用于記錄有關程式運作方式的重要資料:

interface Logger {}      

接下來,向您的界面添加四個方法:

interface Logger {
  debug(message: string, metadata?: Record<string, unknown>): void;
  info(message: string, metadata?: Record<string, unknown>): void;
  warning(message: string, metadata?: Record<string, unknown>): void;
  error(message: string, metadata?: Record<string, unknown>): void;
}      

如代碼塊所示,在接口中建立方法時,我們不會向它們添加任何實作,隻添加它們的類型資訊。

在這種情況下,有四種方法:調試、資訊、警告和錯誤。它們都共享相同的類型簽名:它們接收兩個參數,一個字元串類型的消息和一個可選的 Record<string, unknown> 類型的中繼資料參數,它們都傳回 void 類型。

實作此接口的所有類都必須為這些方法中的每一個具有相應的參數和傳回類型。在名為 ConsoleLogger 的類中實作接口,該類使用控制台方法記錄所有消息:

class ConsoleLogger implementsLogger {
  debug(message: string, metadata?: Record<string, unknown>) {
    console.info(`[DEBUG] ${message}`, metadata);
  }
  info(message: string, metadata?: Record<string, unknown>) {
    console.info(message, metadata);
  }
  warning(message: string, metadata?: Record<string, unknown>) {
    console.warn(message, metadata);
  }
  error(message: string, metadata?: Record<string, unknown>) {
    console.error(message, metadata);
  }
}      

注意,在建立接口時,我們使用了一個名為 implements 的新關鍵字來指定類實作的接口清單。

可以通過在 implements 關鍵字之後将它們添加為以逗号分隔的接口辨別符清單來實作多個接口。例如,如果有另一個名為 Clearable 的接口:

interface Clearable {
  clear(): void;
}      

我們可以通過添加以下突出顯示的代碼在 ConsoleLogger 類中實作它:

class ConsoleLogger implements Logger, Clearable{
  clear() {
    console.clear();
  }
  debug(message: string, metadata?: Record<string, unknown>) {
    console.info(`[DEBUG] ${message}`, metadata);
  }
  info(message: string, metadata?: Record<string, unknown>) {
    console.info(message, metadata);
  }
  warning(message: string, metadata?: Record<string, unknown>) {
    console.warn(message, metadata);
  }
  error(message: string, metadata?: Record<string, unknown>) {
    console.error(message, metadata);
  }
}      

請注意,必須添加 clear 方法以確定該類遵循新接口。

如果沒有為任何接口所需的成員之一提供實作,例如 Logger 接口中的調試方法,TypeScript 編譯器會給您錯誤 2420:

Output Class 'ConsoleLogger' incorrectly implements interface 'Logger'.
  Property 'debug' is missing in type 'ConsoleLogger' but required in type 'Logger'. (2420)      

如果我們的實作與我們正在實作的接口所期望的不比對,TypeScript 編譯器也會顯示錯誤。例如,如果将調試方法中的消息參數類型從字元串更改為數字,我們将收到錯誤 2416:

OutputProperty 'debug' in type 'ConsoleLogger' is not assignable to the same property in base type 'Logger'.
  Type '(message: number, metadata?: Record<string, unknown> | undefined) => void' is not assignable to type '(message: string, metadata: Record<string, unknown>) => void'.
    Types of parameters 'message' and 'message' are incompatible.
      Type 'string' is not assignable to type 'number'. (2416)      

建立在抽象類之上

抽象類與普通類類似,有兩個主要差別:它們不能直接執行個體化,并且可能包含抽象元素。抽象元素是必須在繼承類中實作的元素。它們在抽象類本身中沒有實作。

這很有用,因為我們可以在基本抽象類中擁有一些通用功能,并在繼承類中擁有更具體的實作。當我們将一個類标記為抽象時,我們是在說這個類缺少應該在繼承類中實作的功能。

要建立抽象類,請在 class 關鍵字之前添加 abstract 關鍵字,如突出顯示的代碼:

abstractclass AbstractClassName {


}      

接下來,我們可以在抽象類中建立對象,其中一些可能有實作,而另一些則沒有。沒有實作的被标記為抽象,然後必須在從抽象類擴充的類中實作。

例如,假設我們在 Node.js 環境中工作,并且正在建立自己的 Stream 實作。為此,我們将擁有一個名為 Stream 的抽象類,它有兩個抽象類,read和write:

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}


abstract class Stream {


  abstract read(count: number): Buffer;


  abstract write(data: Buffer): void;
}      

這裡的 Buffer 對象是 Node.js 中可用的一個類,用于存儲二進制資料。頂部的 declare class Buffer 語句允許代碼在沒有 Node.js 類型聲明的 TypeScript 環境中編譯,例如 TypeScript Playground。

在這個例子中,read 方法從内部資料結構中計算位元組數并傳回一個 Buffer 對象,并将執行個體的write所有内容寫入Buffer流中。這兩種方法都是抽象的,隻能在從 Stream 擴充的類中實作。

然後,我們可以建立具有實作的其他方法。這樣,從Stream 抽象類擴充的任何類都将自動接收這些方法。copy方法執行個體如下:

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}


abstract class Stream {


  abstract read(count: number): Buffer;


  abstract write(data: Buffer): void;


  copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
    const data = this.read(count);
    data.copy(targetBuffer, targetBufferOffset);
  }
}      

copy方法将從流中讀取位元組的結果複制到 targetBuffer,從 targetBufferOffset 開始。

如果我們為 Stream 抽象類建立一個實作,如 FileStream 類,則複制方法将很容易使用,而無需在 FileStream 類中複制它:

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}


abstract class Stream {


  abstract read(count: number): Buffer;


  abstract write(data: Buffer): void;


  copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
    const data = this.read(count);
    data.copy(targetBuffer, targetBufferOffset);
  }
}


class FileStream extends Stream {
  read(count: number): Buffer {
    // implementation here
    return new Buffer();
  }


  write(data: Buffer) {
    // implementation here
  }
}


const fileStream = new FileStream();      

在此示例中,fileStream 執行個體自動具有可用的copy方法。FileStream 類還必須顯示實作讀取和寫入方法以遵守 Stream 抽象類。

如果我們忘記要從中擴充的抽象類中抽象某元素,例如,未在 FileStream 類中添加write,TypeScript 編譯器将給出錯誤 2515:

OutputNon-abstract class 'FileStream' does not implement inherited abstract member 'write' from class 'Stream'. (2515)      

如果我們錯誤地實作了任何元素,TypeScript 編譯器也會顯示錯誤,例如,将 write 方法的第一個參數的類型更改為 string 類型而不是 Buffer:

OutputProperty 'write' in type 'FileStream' is not assignable to the same property in base type 'Stream'.
  Type '(data: string) => void' is not assignable to type '(data: Buffer) => void'.
    Types of parameters 'data' and 'data' are incompatible.
      Type 'Buffer' is not assignable to type 'string'. (2416)      

使用抽象類和接口,我們可以對類5+進行更複雜的類型檢查,以確定從基類擴充的類繼承正确的功能。接下來,您将通過示例了解 TypeScript 中的方法和屬性可見性如何工作。

類成員可見性

TypeScript 通過允許指定類成員的可見性來增強可用的 JavaScript 類文法。在這種情況下,可見性是指執行個體化類之外的代碼如何與類内的成員互動。

TypeScript 中的類成員可能具有三種可能的可見性修飾符:public、protected 和 private。

公共成員可以在類執行個體之外通路,而私有成員則不能。protected 介于兩者之間,其中成員可以由類的執行個體或基于該類的子類通路。

在本節中,我們将檢查可用的可見性修飾符并了解它們的含義。

public

這是 TypeScript 中類成員的預設可見性。當我們不将可見性修飾符添加到類成員時,它與将其設定為 public 相同。公共類成員可以在任何地方通路,沒有任何限制。

為了說明這一點,回到前面的 Person 類:

class Person {
  public instantiatedAt = new Date();


  constructor(
    name: string,
    age: number
) {}
}      

本教程提到,預設情況下,name和age這兩個屬性具有公開可見性。要顯示聲明類型可見性,請在屬性之前添加 public 關鍵字,并為類添加一個名為 getBirthYear 的新公共方法,該方法檢索 Person 執行個體的出生年份:

class Person {
  constructor(
    publicname: string,
    publicage: number
) {}


  public getBirthYear() {
    return new Date().getFullYear() - this.age;
  }
}      

然後,我們可以在類執行個體之外的全局空間中使用屬性和方法:

class Person {
  constructor(
    public name: string,
    public age: number
) {}


  public getBirthYear() {
    return new Date().getFullYear() - this.age;
  }
}


const jon = new Person("Jon", 35);


console.log(jon.name);
console.log(jon.age);
console.log(jon.getBirthYear());      

此代碼會将以下内容列印到控制台:

OutputJon
35
1986      

請注意,我們可以通路班級的所有成員。

protected

具有受保護可見性的類成員隻允許在聲明它們的類内部或該類的子類中使用。

看看下面的 Employee 類和基于它的 FinanceEmployee 類:

class Employee {
  constructor(
    protected identifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}      

突出顯示的代碼顯示了聲明為受保護可見性的辨別符屬性。this.identifier 代碼嘗試從 FinanceEmployee 子類通路此屬性。此代碼将在 TypeScript 中運作而不會出錯。

如果嘗試從不在類本身或子類内部的位置使用該方法,如下例所示:

class Employee {
  constructor(
    protected identifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}


const financeEmployee = new FinanceEmployee('abc-12345');
financeEmployee.identifier;      

TypeScript 編譯器會給我們報出錯誤,2445:

OutputProperty 'identifier' is protected and only accessible within class 'Employee' and its subclasses. (2445)      

這是因為無法從全局空間中檢索到新的 financeEmployee 執行個體的辨別符屬性。相反,必須使用内部方法 getFinanceIdentifier 來傳回包含辨別符屬性的字元串:

class Employee {
  constructor(
    protected identifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}


const financeEmployee = new FinanceEmployee('abc-12345');
console.log(financeEmployee.getFinanceIdentifier())      

這會将以下内容記錄到控制台:

Output fin-abc-12345      

private

私有成員隻能在聲明它們的類内部通路。這意味着即使是子類也無法通路它。

使用前面的示例,将 Employee 類中的辨別符屬性轉換為私有屬性:​

class Employee {
  constructor(
    privateidentifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}      

此代碼現在将導緻 TypeScript 編譯器顯示錯誤 2341:​

Output 
Property 'identifier' is private and only accessible within class 'Employee'. (2341)      

發生這種情況是因為我們正在通路 FinanceEmployee 子類中的屬性辨別符,這是不允許的,因為辨別符屬性是在 Employee 類中聲明的,并且其可見性設定為私有。

請記住,TypeScript 被編譯為原始 JavaScript,其本身無法指定類成員的可見性。是以,TypeScript 在運作時沒有針對此類使用的保護。這是 TypeScript 編譯器僅在編譯期間完成的安全檢查。

現在,已經嘗試了可見性修飾符,我們可以繼續使用箭頭函數作為 TypeScript 類中的方法。

類方法作為箭頭函數

在 JavaScript 中,表示函數上下文的 this 值可以根據函數的調用方式而改變。這種可變性有時會在複雜的代碼中造成混淆。使用 TypeScript 時,可以在建立類方法時使用特殊文法,以避免将其綁定到類執行個體以外的其他東西。在本節中,我們将嘗試這種文法。

使用 Employee 類,引入一個僅用于檢索員工辨別符的新方法:​

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier() {
  return this.identifier;
  }
}      

如果直接調用該方法,這将非常有效:

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier() {
    return this.identifier;
  }
}


const employee = new Employee("abc-123");


console.log(employee.getIdentifier());      

這會将以下内容列印到控制台的輸出:​

Output abc-123      

但是,如果将 getIdentifier 執行個體方法存儲在某處以供稍後調用,如以下代碼所示:

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier() {
    return this.identifier;
  }
}


const employee = new Employee("abc-123");


const obj = {
  getId: employee.getIdentifier
}


console.log(obj.getId());      

該值将無法通路:​

Output undefined      

發生這種情況是因為當我們調用 obj.getId() 時,employee.getIdentifier 内部的 this 現在綁定到 obj 對象,而不是 Employee 執行個體。

可以通過将 getIdentifier 更改為箭頭函數來避免這種情況。檢查以下代碼中突出顯示的更改:

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier = () => {
    return this.identifier;
  }
}
...      

如果現在嘗試像以前一樣調用 obj.getId(),控制台會正确顯示:​

abc-123      

這示範了 TypeScript 如何允許箭頭函數用作類方法的直接值。

在下一節中,将學習如何使用 TypeScript 的類型檢查來強制類。

使用類作為類型

到目前為止,本教程已經介紹了如何建立類并直接使用它們。

在本節中,我們将在使用 TypeScript 時将類用作類型。

類在 TypeScript 中既是類型又是值,是以,可兩種方式使用。要将類用作類型,請在 TypeScript 需要類型的任何地方使用類名。例如,給定之前建立的 Employee 類:​

class Employee {
  constructor(
    public identifier: string
) {}
}      

想象一下,我們想建立一個列印任何員工辨別符的函數。我們可以像這樣建立這樣的函數:​

class Employee {
  constructor(
    public identifier: string
) {}
}


function printEmployeeIdentifier(employee: Employee) {
  console.log(employee.identifier);
}      

請注意,将employee 參數設定為Employee 類型,這是類的确切名稱。

TypeScript 中的類與其他類型(包括其他類)進行比較,就像在 TypeScript 中比較其他類型一樣:在結構上。這意味着,如果有兩個具有相同形狀的不同類(即具有相同可見性的同一組成員),則它們可以在隻期望其中一個的地方互換使用。

為了說明這一點,假設我們的應用程式中有另一個名為 Warehouse 的類:​

class Warehouse {
  constructor(
    public identifier: string
) {}
}      

它的形狀與 Employee 相同。如果嘗試将其執行個體傳遞給 printEmployeeIdentifier:

class Employee {
  constructor(
    public identifier: string
) {}
}


class Warehouse {
  constructor(
    public identifier: string
) {}
}


function printEmployeeIdentifier(employee: Employee) {
  console.log(employee.identifier);
}


const warehouse = new Warehouse("abc");


printEmployeeIdentifier(warehouse);      

TypeScript 編譯器不會抱怨。甚至可以隻使用普通對象而不是類的執行個體。由于這可能會導緻剛開始使用 TypeScript 的程式員所不期望的行為,是以密切關注這些情況非常重要。

通過使用類作為類型的基礎知識,現在可以學習如何檢查特定類,而不僅僅是形狀。

this類型

有時,我們需要在類本身的某些方法中引用目前類的類型。

在本節中,我們将了解如何使用它來完成此操作。

想象一下,必須向 Employee 類添加一個名為 isSameEmployeeAs 的新方法,該方法将負責檢查另一個員工執行個體是否引用與目前員工相同的員工。可以這樣做,方法如下:

class Employee {
  constructor(
    protected identifier: string
  ) {}


  getIdentifier() {
    return this.identifier;
  }


  isSameEmployeeAs(employee: Employee) {
    return this.identifier === employee.identifier;
  }
}      

該測試将用于比較從 Employee 派生的所有類的辨別符屬性。但是想象一個場景,根本不希望比較 Employee 的特定子類。在這種情況下,希望 TypeScript 在比較兩個不同的子類時報告錯誤,而不是接收比較的布爾值。

例如,為财務和營銷部門的員工建立兩個新的子類:

...
class FinanceEmployee extends Employee {
  specialFieldToFinanceEmployee = '';
}


class MarketingEmployee extends Employee {
  specialFieldToMarketingEmployee = '';
}


const finance = new FinanceEmployee("fin-123");
const marketing = new MarketingEmployee("mkt-123");


marketing.isSameEmployeeAs(finance);      

在這裡,從 Employee 基類派生了兩個類:FinanceEmployee 和 MarketingEmployee。每個都有不同的新領域。

然後,将為每個執行個體建立一個執行個體,并檢查營銷員工是否與财務員工相同。鑒于這種情況,TypeScript 應該報告錯誤,因為根本不應該比較子類。

這不會發生,因為在 isSameEmployeeAs 方法中使用 Employee 作為員工參數的類型,并且從 Employee 派生的所有類都将通過類型檢查。

要改進此代碼,可以使用類内部可用的特殊類型,即 this 類型。該類型被動态設定為目前類的類型。這樣,當在派生類中調用此方法時,會将 this 設定為派生類的類型。

更改的代碼以使用this:

class Employee {
  constructor(
    protected identifier: string
  ) {}


  getIdentifier() {
    return this.identifier;
  }


  isSameEmployeeAs(employee: this) {
    return this.identifier === employee.identifier;
  }
}


class FinanceEmployee extends Employee {
  specialFieldToFinanceEmployee = '';
}


class MarketingEmployee extends Employee {
  specialFieldToMarketingEmployee = '';
}


const finance = new FinanceEmployee("fin-123");
const marketing = new MarketingEmployee("mkt-123");


marketing.isSameEmployeeAs(finance);      

編譯此代碼時,TypeScript 編譯器現在将顯示錯誤 2345:

Output Argument of type 'FinanceEmployee' is not assignable to parameter of type 'MarketingEmployee'.
  Property 'specialFieldToMarketingEmployee' is missing in type 'FinanceEmployee' but required in type 'MarketingEmployee'. (2345)      

使用 this 關鍵字,可以在不同的類上下文中動态更改類型。接下來,将使用類型來傳遞類本身,而不是類的執行個體。

使用構造簽名

有時程式員需要建立一個直接采用類而不是執行個體的函數。為此,需要使用帶有構造簽名的特殊類型。

在本節中,将了解如何建立此類類型。

可能需要傳入類本身的一個特定場景是類工廠,或生成作為參數傳入的類的新執行個體的函數。想象一下,建立一個函數,該函數接受一個基于 Employee 的類,建立一個具有遞增辨別符的新執行個體,并将辨別符列印到控制台。可以嘗試如下建立:

class Employee {
  constructor(
    public identifier: string
) {}
}


let identifier = 0;
function createEmployee(ctor: Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}      

在此代碼段中,将建立 Employee 類,初始化辨別符,并建立一個函數,該函數基于具有 Employee 形狀的構造函數參數 ctor 執行個體化一個類。但是如果試圖編譯這段代碼,TypeScript 編譯器會給出錯誤 2351:

Output This expression is not constructable.
  Type 'Employee' has no construct signatures. (2351)      

發生這種情況是因為,當使用類的名稱作為 ctor 的類型時,該類型僅對類的執行個體有效。要擷取類構造函數本身的類型,必須使用 typeof ClassName。檢查以下突出顯示的代碼進行更改:

class Employee {
  constructor(
    public identifier: string
) {}
}


let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}      

現在的代碼将成功編譯。但是還有一個懸而未決的問題:由于類工廠建構了從基類建構的新類的執行個體,是以,使用抽象類可以改進工作流程。但是,這最初不起作用。

要嘗試這一點,請将 Employee 類轉換為abstract(抽象)類:

abstractclass Employee {
  constructor(
    public identifier: string
) {}
}


let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}      

TypeScript 編譯器現在将給出錯誤,2511:

Output Cannot create an instance of an abstract class. (2511)      

此錯誤表明無法從 Employee 類建立執行個體,因為它是抽象的。但是,可能希望使用這樣的函數來建立從 Employee 抽象類擴充而來的不同類型的員工,例如:

abstract class Employee {
  constructor(
    public identifier: string
  ) {}
}


class FinanceEmployee extends Employee {}


class MarketingEmployee extends Employee {}


let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}


createEmployee(FinanceEmployee);
createEmployee(MarketingEmployee);      

要使代碼适用于這種情況,必須使用具有構造函數簽名的類型。可以使用 new 關鍵字,後跟類似于箭頭函數的文法來執行此操作,其中參數清單包含構造函數預期的參數,傳回類型是此構造函數傳回的類執行個體。

以下代碼中突出顯示的更改是将具有構造函數簽名的類型引入 createEmployee 函數:

abstract class Employee {
  constructor(
    public identifier: string
) {}
}


class FinanceEmployee extends Employee {}


class MarketingEmployee extends Employee {}


let identifier = 0;
function createEmployee(ctor: new (identifier: string) => Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}


createEmployee(FinanceEmployee);
createEmployee(MarketingEmployee);      

TypeScript 編譯器現在将正确編譯代碼。

結論

TypeScript 中的類比 JavaScript 中的類更強大,因為可以通路類型系統、箭頭函數方法等額外文法以及成員可見性和抽象類等全新功能。這為我們提供了一種傳遞類型安全、更可靠且更好地代表應用程式業務模型的代碼的方法。

學習更多技能

請點選下方公衆号

繼續閱讀