類別

背景閱讀
類別 (MDN)

TypeScript 完全支援 ES2015 中引入的 class 關鍵字。

與其他 JavaScript 語言功能一樣,TypeScript 新增類型註解和其他語法,讓您表達類別與其他類型之間的關係。

類別成員

以下是基本類別 - 空類別

ts
class Point {}
Try

這個類別目前沒有什麼用,讓我們開始加入一些成員。

欄位

欄位宣告會在類別中建立一個可寫入的公開屬性

ts
class Point {
x: number;
y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;
Try

與其他位置一樣,類型註解是可選的,但如果未指定,會是隱含的 any

欄位也可以有初始化項;這些初始化項會在類別實例化時自動執行

ts
class Point {
x = 0;
y = 0;
}
 
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);
Try

就像 constletvar 一樣,類別屬性的初始化項會用來推斷其類型

ts
const pt = new Point();
pt.x = "0";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

--strictPropertyInitialization

strictPropertyInitialization 設定控制類別欄位是否需要在建構函式中初始化。

ts
class BadGreeter {
name: string;
Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.
}
Try
ts
class GoodGreeter {
name: string;
 
constructor() {
this.name = "hello";
}
}
Try

請注意,欄位需要在建構函式本身中初始化。TypeScript 沒有分析您從建構函式呼叫的方法,以偵測初始化,因為衍生類別可能會覆寫這些方法,而無法初始化成員。

如果您打算透過建構函式以外的方法明確初始化欄位(例如,外部函式庫可能為您填寫部分類別),您可以使用明確指定運算子!

ts
class OKGreeter {
// Not initialized, but no error
name!: string;
}
Try

readonly

欄位可以加上 readonly 修飾詞。這會防止在建構函式外部指定欄位。

ts
class Greeter {
readonly name: string = "world";
 
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
 
err() {
this.name = "not ok";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
Try

建構函式

背景閱讀
建構函式 (MDN)

類別建構函式與函式非常相似。你可以加入具有型別註解、預設值和重載的參數

ts
class Point {
x: number;
y: number;
 
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
Try
ts
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
Try

類別建構函式簽章和函式簽章之間只有幾個不同

  • 建構函式無法有型別參數 - 這些屬於外部類別宣告,我們稍後會學習
  • 建構函式無法有傳回型別註解 - 類別實例型別永遠是傳回的內容

超級呼叫

就像在 JavaScript 中,如果你有一個基底類別,你需要在使用任何 this. 成員之前在建構函式主體中呼叫 super();

ts
class Base {
k = 4;
}
 
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
Try

忘記呼叫 super 是 JavaScript 中容易犯的錯誤,但 TypeScript 會在你需要時告訴你。

方法

背景閱讀
方法定義

類別上的函式屬性稱為方法。方法可以使用與函式和建構函式相同的類型註解

ts
class Point {
x = 10;
y = 10;
 
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
Try

除了標準類型註解之外,TypeScript 沒有為方法新增任何其他新功能。

請注意,在方法主體內,仍然必須透過 this. 來存取欄位和其他方法。方法主體中未限定的名稱將永遠參考封裝範圍內的某個項目

ts
let x: number = 0;
 
class C {
x: string = "hello";
 
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
}
}
Try

取得器 / 設定器

類別也可以有存取器

ts
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
Try

請注意,在 JavaScript 中,沒有額外邏輯的欄位後援取得/設定配對很少有用。如果您不需要在取得/設定作業期間新增額外邏輯,公開欄位是可以的。

TypeScript 對存取器有一些特殊的推論規則

  • 如果存在 get 但沒有 set,則屬性會自動為 readonly
  • 如果未指定設定器參數的類型,則會從取得器的傳回類型推論
  • 取得器和設定器必須具有相同的 成員可見性

TypeScript 4.3 以來,可以有取得和設定不同類型的存取器。

ts
class Thing {
_size = 0;
 
get size(): number {
return this._size;
}
 
set size(value: string | number | boolean) {
let num = Number(value);
 
// Don't allow NaN, Infinity, etc
 
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
 
this._size = num;
}
}
Try

索引簽章

類別可以宣告索引簽章;這些簽章與其他物件類型的索引簽章相同

ts
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
 
check(s: string) {
return this[s] as boolean;
}
}
Try

因為索引簽章類型也需要擷取方法的類型,所以不容易使用這些類型。通常最好將索引資料儲存在其他地方,而不是儲存在類別實例本身。

類別傳承

與其他具有物件導向功能的語言一樣,JavaScript 中的類別可以繼承自基礎類別。

implements 子句

你可以使用 implements 子句檢查類別是否符合特定 interface。如果類別未正確實作,系統會發出錯誤訊息

ts
interface Pingable {
ping(): void;
}
 
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
 
class Ball implements Pingable {
Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.2420Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
pong() {
console.log("pong!");
}
}
Try

類別也可以實作多個介面,例如 class C implements A, B {

注意事項

了解 implements 子句只是檢查類別是否可以視為介面類型非常重要。它不會改變類別或其方法的類型。常見的錯誤來源是假設 implements 子句會改變類別類型 - 它不會!

ts
interface Checkable {
check(name: string): boolean;
}
 
class NameChecker implements Checkable {
check(s) {
Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.
// Notice no error here
return s.toLowerCase() === "ok";
any
}
}
Try

在此範例中,我們可能預期 s 的類型會受到 checkname: string 參數影響。並非如此 - implements 子句不會改變類別主體的檢查方式或其推論的類型。

類似地,實作具有選用屬性的介面不會建立該屬性

ts
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;
Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.
Try

extends 子句

背景閱讀
extends 關鍵字 (MDN)

類別可以從基底類別中extend。衍生類別具有其基底類別的所有屬性和方法,並且也可以定義其他成員。

ts
class Animal {
move() {
console.log("Moving along!");
}
}
 
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
 
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);
Try

覆寫方法

背景閱讀
super 關鍵字 (MDN)

衍生類別也可以覆寫基底類別的欄位或屬性。您可以使用super. 語法存取基底類別的方法。請注意,由於 JavaScript 類別是一個簡單的查詢物件,因此沒有「super 欄位」的概念。

TypeScript 強制執行衍生類別永遠是其基底類別的子類型。

例如,以下是覆寫方法的合法方式

ts
class Base {
greet() {
console.log("Hello, world!");
}
}
 
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
 
const d = new Derived();
d.greet();
d.greet("reader");
Try

重要的是,衍生類別必須遵循其基底類別的合約。請記住,透過基底類別參考來參考衍生類別實例是非常常見(而且永遠都是合法的)

ts
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
Try

如果Derived 沒有遵循Base 的合約會怎樣?

ts
class Base {
greet() {
console.log("Hello, world!");
}
}
 
class Derived extends Base {
// Make this parameter required
greet(name: string) {
Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.2416Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.
console.log(`Hello, ${name.toUpperCase()}`);
}
}
Try

如果我們編譯此程式碼,即使有錯誤,此範例也會崩潰

ts
const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();
Try

僅類型欄位宣告

target >= ES2022useDefineForClassFieldstrue,類別欄位會在父類別建構函式完成後初始化,覆寫父類別設定的任何值。當您只想重新宣告繼承欄位的更精確類型時,這可能會造成問題。為了解決這些情況,您可以撰寫 declare 來指示 TypeScript 此欄位宣告不應有執行時期效果。

ts
interface Animal {
dateOfBirth: any;
}
 
interface Dog extends Animal {
breed: any;
}
 
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
 
class DogHouse extends AnimalHouse {
// Does not emit JavaScript code,
// only ensures the types are correct
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
Try

初始化順序

在某些情況下,JavaScript 類別初始化順序可能會令人驚訝。我們來考慮以下程式碼

ts
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
 
class Derived extends Base {
name = "derived";
}
 
// Prints "base", not "derived"
const d = new Derived();
Try

這裡發生了什麼事?

JavaScript 定義的類別初始化順序為

  • 初始化基本類別欄位
  • 執行基本類別建構函式
  • 初始化衍生類別欄位
  • 執行衍生類別建構函式

這表示基本類別建構函式在自己的建構函式期間看到了 name 的自身值,因為衍生類別欄位初始化尚未執行。

繼承內建型別

注意:如果您不打算繼承內建型別,例如 ArrayErrorMap 等,或者您的編譯目標明確設定為 ES6/ES2015 或更高,您可以跳過本節

在 ES2015 中,傳回物件的建構函式會隱式地將 this 的值替換為任何呼叫 super(...) 的呼叫者。產生的建構函式程式碼必須擷取 super(...) 的任何潛在傳回值,並將其替換為 this

因此,繼承 ErrorArray 等可能無法再如預期般運作。這是因為 ErrorArray 等的建構函式會使用 ECMAScript 6 的 new.target 來調整原型鏈;然而,在 ECMAScript 5 中呼叫建構函式時,無法確保 new.target 的值。其他下階編譯器通常預設有相同的限制。

對於以下類型的子類別

ts
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
Try

您可能會發現

  • 在建構這些子類別傳回的物件上,方法可能是 undefined,因此呼叫 sayHello 會導致錯誤。
  • instanceof 會在子類別的執行個體及其執行個體之間中斷,因此 (new MsgError()) instanceof MsgError 會傳回 false

建議您在任何 super(...) 呼叫之後,立即手動調整原型。

ts
class MsgError extends Error {
constructor(m: string) {
super(m);
 
// Set the prototype explicitly.
Object.setPrototypeOf(this, MsgError.prototype);
}
 
sayHello() {
return "hello " + this.message;
}
}
Try

不過,MsgError 的任何子類別都必須手動設定原型。對於不支援 Object.setPrototypeOf 的執行時期,您可能可以使用 __proto__

很不幸地,這些解決方法不適用於 Internet Explorer 10 及更早版本。您可手動將方法從原型複製到執行個體本身 (例如,將 MsgError.prototype 複製到 this),但原型鏈本身無法修復。

成員可見性

您可以使用 TypeScript 來控制特定方法或屬性是否對類別外的程式碼可見。

public

類別成員的預設可見性為 publicpublic 成員可以在任何地方存取

ts
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
Try

由於 public 已是預設的可見性修飾詞,因此您不需要在類別成員中撰寫它,但可以出於樣式/可讀性原因選擇這樣做。

protected

protected 成員僅對其宣告所在的類別的子類別可見。

ts
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
 
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Try

受保護成員的公開

衍生類別需要遵循其基底類別合約,但可能會選擇公開具有更多功能的基底類別子類型。這包括將受保護成員設為公開

ts
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
Try

請注意,Derived已經可以自由讀寫m,因此這不會有意義地改變此情況的「安全性」。這裡要注意的主要事項是,在衍生類別中,如果這種公開並非有意為之,我們需要小心重複受保護修飾詞。

跨層級受保護存取

不同的 OOP 語言對於是否可以透過基底類別參考存取受保護成員有不同的看法

ts
class Base {
protected x: number = 1;
}
class Derived1 extends Base {
protected x: number = 5;
}
class Derived2 extends Base {
f1(other: Derived2) {
other.x = 10;
}
f2(other: Derived1) {
other.x = 10;
Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.2445Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
}
}
Try

例如,Java 認為這是合法的。另一方面,C# 和 C++ 則選擇此程式碼應該是違法的。

TypeScript 在這裡與 C# 和 C++ 站同一陣線,因為在 Derived2 中存取 x 應該只允許 Derived2 的子類別,而 Derived1 並非其中之一。此外,如果透過 Derived1 參照存取 x 是非法的(這當然應該是如此!),那麼透過基底類別參照存取它絕不會改善情況。

另請參閱 為何我無法從衍生類別存取受保護的成員?,其中進一步說明 C# 的推理。

private

private 類似於 protected,但即使從子類別也無法存取成員

ts
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
Property 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.
Try
ts
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
Property 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.
}
}
Try

由於 private 成員對衍生類別不可見,因此衍生類別無法增加其可見性

ts
class Base {
private x = 0;
}
class Derived extends Base {
Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.
x = 1;
}
Try

跨執行個體的 private 存取

不同的 OOP 語言對於同一個類別的不同執行個體是否可以存取彼此的 private 成員有不同的看法。Java、C#、C++、Swift 和 PHP 等語言允許這麼做,但 Ruby 則不允許。

TypeScript 允許跨實例存取 private

ts
class A {
private x = 10;
 
public sameAs(other: A) {
// No error
return other.x === this.x;
}
}
Try

注意事項

與 TypeScript 類型系統的其他面向一樣,privateprotected 僅在類型檢查期間強制執行

這表示,JavaScript 執行時期建構,例如 in 或簡單的屬性查詢,仍可存取 privateprotected 成員

ts
class MySafe {
private secretKey = 12345;
}
Try
js
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

private 也允許在類型檢查期間使用方括號表示法存取。這使得 private 宣告的欄位可能更容易讓單元測試等項目存取,缺點是這些欄位是軟私有,不會嚴格強制執行私密性。

ts
class MySafe {
private secretKey = 12345;
}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
Property 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);
Try

與 TypeScript 的 private 不同,JavaScript 的 私有欄位 (#) 在編譯後仍保持私有,且不會提供先前提到的逃生艙口,例如方括號表示法存取,使其成為硬私有

ts
class Dog {
#barkAmount = 0;
personality = "happy";
 
constructor() {}
}
Try
ts
"use strict";
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() { }
}
 
Try

在編譯到 ES2021 或更低版本時,TypeScript 會使用 WeakMap 取代 #

ts
"use strict";
var _Dog_barkAmount;
class Dog {
constructor() {
_Dog_barkAmount.set(this, 0);
this.personality = "happy";
}
}
_Dog_barkAmount = new WeakMap();
 
Try

如果您需要保護類別中的值免於惡意行為者,您應該使用提供硬執行時期私密性的機制,例如封閉、WeakMap 或私有欄位。請注意,這些在執行時期新增的私密性檢查可能會影響效能。

靜態成員

背景閱讀
靜態成員 (MDN)

類別可以有static成員。這些成員與類別的特定實例無關。它們可以透過類別建構函式物件本身來存取

ts
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
Try

靜態成員也可以使用相同的publicprotectedprivate可見度修飾詞

ts
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.
Try

靜態成員也會被繼承

ts
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
Try

特殊靜態名稱

通常無法安全地覆寫 Function 原型的屬性。由於類別本身是可使用 new 呼叫的函式,因此無法使用某些 static 名稱。函式屬性(例如 namelengthcall)無法定義為 static 成員

ts
class S {
static name = "S!";
Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
Try

為何沒有靜態類別?

TypeScript(和 JavaScript)沒有像 C# 那樣的 static class 建構函式。

這些建構函式存在於這些語言強制所有資料和函式都必須在類別內部的情況下;由於 TypeScript 中不存在這種限制,因此不需要這些建構函式。在 JavaScript/TypeScript 中,只有一個執行個體的類別通常只表示為一個正常的物件

例如,我們不需要 TypeScript 中的「靜態類別」語法,因為一般物件(甚至是頂層函式)也能執行相同的工作

ts
// Unnecessary "static" class
class MyStaticClass {
static doSomething() {}
}
 
// Preferred (alternative 1)
function doSomething() {}
 
// Preferred (alternative 2)
const MyHelperObject = {
dosomething() {},
};
Try

類別中的static區塊

靜態區塊允許您撰寫一系列陳述式,其具有自己的範圍,可以存取包含類別中的私有欄位。這表示我們可以使用撰寫陳述式的所有功能撰寫初始化程式碼,不會洩漏變數,而且可以完全存取我們類別的內部。

ts
class Foo {
static #count = 0;
 
get count() {
return Foo.#count;
}
 
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
Try

泛型類別

類別(很像介面)可以是泛型的。當使用 new 執行泛型類別的實例化時,其類型參數的推論方式與函式呼叫相同

ts
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
 
const b = new Box("hello!");
const b: Box<string>
Try

類別可以使用泛型約束和預設值,方式與介面相同。

靜態成員中的類型參數

這段程式碼是非法的,而且可能不容易理解原因

ts
class Box<Type> {
static defaultValue: Type;
Static members cannot reference class type parameters.2302Static members cannot reference class type parameters.
}
Try

請記住,類型總是會被完全清除!在執行階段,只有一個 Box.defaultValue 屬性槽。這表示設定 Box<string>.defaultValue(如果可行)也會變更 Box<number>.defaultValue - 不好。泛型類別的 static 成員永遠無法參考類別的類型參數。

this 在類別中的執行階段

背景閱讀
this 關鍵字 (MDN)

重要的是要記住,TypeScript 沒有改變 JavaScript 的執行階段行為,而 JavaScript 以具有某些奇特的執行階段行為而聞名。

JavaScript 處理 this 的方式確實不尋常

ts
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
 
// Prints "obj", not "MyClass"
console.log(obj.getName());
Try

長話短說,預設情況下,函式內 this 的值取決於函式被呼叫的方式。在此範例中,由於函式是透過 obj 參照被呼叫的,因此其 this 的值是 obj,而不是類別實例。

這很少是你想要發生的情況!TypeScript 提供了一些方法來減輕或防止這種錯誤。

箭頭函式

背景閱讀
箭頭函式 (MDN)

如果您有一個函式會經常以失去其 this 內容的方式被呼叫,那麼使用箭頭函式屬性會比使用方法定義更有意義

ts
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());
Try

這有一些權衡

  • this 值保證在執行時是正確的,即使是未經 TypeScript 檢查的程式碼
  • 這將使用更多記憶體,因為每個類別實例都會有以這種方式定義的每個函式的副本
  • 您無法在衍生類別中使用 super.getName,因為原型鏈中沒有條目可以從中擷取基底類別方法

this 參數

在方法或函式定義中,名為 this 的初始參數在 TypeScript 中具有特殊意義。這些參數會在編譯期間被刪除

ts
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
/* ... */
}
Try
js
// JavaScript output
function fn(x) {
/* ... */
}

TypeScript 檢查呼叫帶有 this 參數的函式是否使用正確的內容。我們可以將 this 參數新增到方法定義中,以靜態強制執行方法正確呼叫,而不是使用箭頭函式

ts
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
 
// Error, would crash
const g = c.getName;
console.log(g());
The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
Try

這種方法與箭頭函式方法產生相反的權衡

  • JavaScript 呼叫者可能仍然會在不知情的情況下不正確地使用類別方法
  • 每個類別定義只會分配一個函式,而不是每個類別實例一個函式
  • 基本方法定義仍可透過 super 呼叫。

this 類型

在類別中,稱為 this 的特殊類型會動態地參考目前類別的類型。讓我們看看這有什麼用處

ts
class Box {
contents: string = "";
set(value: string) {
(method) Box.set(value: string): this
this.contents = value;
return this;
}
}
Try

在此,TypeScript 將 set 的傳回類型推論為 this,而不是 Box。現在讓我們建立 Box 的子類別

ts
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
 
const a = new ClearableBox();
const b = a.set("hello");
const b: ClearableBox
Try

您也可以在參數類型註解中使用 this

ts
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
Try

這與撰寫 other: Box 不同,如果您有派生類別,其 sameAs 方法現在只會接受該派生類別的其他執行個體

ts
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
 
class DerivedBox extends Box {
otherContent: string = "?";
}
 
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
Try

基於 this 的類型守衛

您可以在類別和介面的方法傳回位置中使用 this is Type。當與類型縮小 (例如 if 陳述式) 混用時,目標物件的類型會縮小到指定的 Type

ts
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
 
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
 
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
 
interface Networked {
host: string;
}
 
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
 
if (fso.isFile()) {
fso.content;
const fso: FileRep
} else if (fso.isDirectory()) {
fso.children;
const fso: Directory
} else if (fso.isNetworked()) {
fso.host;
const fso: Networked & FileSystemObject
}
Try

基於 this 的類型守衛的常見用例是允許延遲驗證特定欄位。例如,當驗證 hasValue 為 true 時,此案例會從儲存在 box 內的值中移除 undefined

ts
class Box<T> {
value?: T;
 
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
 
const box = new Box<string>();
box.value = "Gameboy";
 
box.value;
(property) Box<string>.value?: string
 
if (box.hasValue()) {
box.value;
(property) value: string
}
Try

參數屬性

TypeScript 提供特殊語法,用於將建構函數參數轉換為具有相同名稱和值的類別屬性。這些稱為參數屬性,是透過在建構函數參數之前加上可見性修飾詞 publicprivateprotectedreadonly 來建立的。產生的欄位會取得這些修飾詞

ts
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
console.log(a.x);
(property) Params.x: number
console.log(a.z);
Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.
Try

類別表達式

背景閱讀
類別表達式 (MDN)

類別表達式與類別宣告十分類似。唯一的實際差異在於類別表達式不需要名稱,儘管我們可以透過它們最終繫結到的任何識別碼來參照它們

ts
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
 
const m = new someClass("Hello, world");
const m: someClass<string>
Try

建構函數簽章

JavaScript 類別使用 new 算子進行實例化。給定類別本身的類型,InstanceType 實用程式類型會對此操作進行建模。

ts
class Point {
createdAt: number;
x: number;
y: number
constructor(x: number, y: number) {
this.createdAt = Date.now()
this.x = x;
this.y = y;
}
}
type PointInstance = InstanceType<typeof Point>
 
function moveRight(point: PointInstance) {
point.x += 5;
}
 
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8
Try

abstract 類別和成員

TypeScript 中的類別、方法和欄位可能是 abstract

abstract 方法abstract 欄位 是尚未提供實作的欄位。這些成員必須存在於 abstract 類別 中,而 abstract 類別 無法直接實例化。

abstract 類別的角色是作為子類別的基底類別,而子類別會實作所有 abstract 成員。當一個類別沒有任何 abstract 成員時,它被稱為 具體類別

我們來看一個範例

ts
abstract class Base {
abstract getName(): string;
 
printName() {
console.log("Hello, " + this.getName());
}
}
 
const b = new Base();
Cannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.
Try

我們無法使用 new 實例化 Base,因為它是 abstract 的。我們需要建立一個衍生類別並實作 abstract 成員

ts
class Derived extends Base {
getName() {
return "world";
}
}
 
const d = new Derived();
d.printName();
Try

請注意,如果我們忘記實作基底類別的 abstract 成員,我們會收到錯誤訊息

ts
class Derived extends Base {
Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
// forgot to do anything
}
Try

abstract 建構簽章

有時你想要接受一些類別建構函式,它會產生一個類別的實例,而這個類別衍生自一些 abstract 類別。

例如,您可能想要撰寫此程式碼

ts
function greet(ctor: typeof Base) {
const instance = new ctor();
Cannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.
instance.printName();
}
Try

TypeScript 正確地告訴您,您正在嘗試實例化抽象類別。畢竟,根據 `greet` 的定義,撰寫這個程式碼完全合法,而這最終會建構一個抽象類別

ts
// Bad!
greet(Base);
Try

相反地,您想要撰寫一個接受具有建構簽章的函式的函式

ts
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base);
Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.
Try

現在,TypeScript 正確地告訴您哪些類別建構函式可以呼叫 - `Derived` 可以,因為它是具體的,但 `Base` 則不行。

類別之間的關係

在多數情況下,TypeScript 中的類別會以結構化的方式進行比較,與其他型別相同。

例如,這兩個類別可以彼此替換,因為它們相同

ts
class Point1 {
x = 0;
y = 0;
}
 
class Point2 {
x = 0;
y = 0;
}
 
// OK
const p: Point1 = new Point2();
Try

類似地,即使沒有明確的繼承,類別之間也存在子型關係

ts
class Person {
name: string;
age: number;
}
 
class Employee {
name: string;
age: number;
salary: number;
}
 
// OK
const p: Person = new Employee();
Try

這聽起來很簡單,但有一些情況看起來比其他情況更奇怪。

空的類別沒有成員。在結構化型別系統中,沒有成員的型別通常是其他任何東西的超型別。因此,如果您撰寫一個空的類別(不要這樣做!),任何東西都可以用來取代它

ts
class Empty {}
 
function fn(x: Empty) {
// can't do anything with 'x', so I won't
}
 
// All OK!
fn(window);
fn({});
fn(fn);
Try

TypeScript 文件是一個開放原始碼專案。透過 傳送 Pull Request ❤ 來協助我們改善這些頁面

此頁面的貢獻者
RCRyan Cavanaugh (60)
OTOrta Therox (15)
HAHossein Ahmadian-Yazdi (6)
Uuid11 (2)
DSDamanjeet Singh (1)
21+

最後更新:2024 年 3 月 21 日