背景閱讀
類 (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 {
x: number = 0;
y: number = 0;
 
// Constructor overloads
constructor(x: number, y: number);
constructor(xy: string);
constructor(x: string | number, y: number = 0) {
// Code logic here
}
}
Try

類建構函式簽名與函式簽名之間只有幾點差異:

  • 建構函式不能有型別引數——這些屬於外部類宣告,我們稍後會學習。
  • 建構函式不能有返回型別註解——返回的始終是類例項型別。

Super 呼叫

正如在 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

Getter / Setter

類也可以有存取器

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

請注意,在 JavaScript 中,沒有額外邏輯的欄位支援 get/set 對極少有用。如果你不需要在 get/set 操作期間新增額外邏輯,直接暴露公共欄位完全沒問題。

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

  • 如果存在 get 但沒有 set,則屬性自動為 readonly
  • 如果未指定 setter 引數的型別,則會從 getter 的返回型別中推斷得出。

TypeScript 4.3 起,可以為 getter 和 setter 設定不同的型別。

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'. Target signature provides too few arguments. Expected 1 or more, but got 0.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'. Target signature provides too few arguments. Expected 1 or more, but got 0.
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 中,隱式返回物件的建構函式會用呼叫者 super(...) 的返回值替換 this 的值。生成的建構函式程式碼必須捕獲 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.prototypethis 上),但原型鏈本身無法修復。

成員可見性

你可以使用 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

暴露 protected 成員

派生類需要遵循其基類的契約,但可以選擇暴露具有更多功能的基類子型別。這包括將 protected 成員變為 public

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,所以這並不會從根本上改變這種情況的“安全性”。這裡主要要注意的是,在派生類中,如果這種暴露不是故意的,我們需要小心重複使用 protected 修飾符。

跨層級 protected 訪問

TypeScript 不允許在類層級結構中訪問兄弟類的 protected 成員。

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

這是因為在 Derived2 中訪問 x 應該僅對 Derived2 的子類合法,而 Derived1 並不是其中之一。此外,如果透過 Derived1 引用訪問 x 是非法的(這理所應當是非法的!),那麼透過基類引用訪問它也絕不應該改善這種情況。

另請參閱 為什麼我不能從派生類訪問受保護的成員?,文中對 C# 中關於相同主題的推理進行了更多解釋。

private

privateprotected 類似,但即便是子類也不允許訪問該成員。

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

不同的面向物件程式語言對於“同一類的不同例項是否可以訪問彼此的 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 宣告的欄位在單元測試等場景下可能更容易訪問,缺點是這些欄位只是軟私有(soft 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 的 私有欄位 (#) 在編譯後仍然保持私有,並且不提供像括號表示法訪問那樣的逃生艙,使它們成為硬私有(hard private)。

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

這有一些權衡:

  • 即使對於未經過 TypeScript 檢查的程式碼,this 值在執行時也保證是正確的。
  • 這會消耗更多記憶體,因為每個類例項都會擁有以這種方式定義的每個函式的副本。
  • 你不能在派生類中使用 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(抽象的)。

抽象方法抽象欄位 是尚未提供實現的方法或欄位。這些成員必須存在於 抽象類 中,該類不能直接例項化。

抽象類的作用是作為實現所有抽象成員的子類的基類。當一個類沒有任何抽象成員時,它被稱為 具體類(concrete)。

讓我們看一個例子。

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,因為它是抽象的。相反,我們需要建立一個派生類並實現抽象成員。

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

請注意,如果我們忘記實現基類的抽象成員,我們將收到一個錯誤。

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

抽象構造簽名

有時你想要接受某種產生從特定抽象類派生的例項的類建構函式。

例如,你可能想要編寫這段程式碼。

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)
MRMaxim R (3)
Uuid11 (2)
23+

最後更新:2026 年 3 月 27 日