背景閱讀
類 (MDN)
TypeScript 完全支援 ES2015 中引入的 class 關鍵字。
與其他 JavaScript 語言特性一樣,TypeScript 添加了型別註解和其他語法,允許你表達類與其他型別之間的關係。
類成員
這是最基本的類——一個空類
tsTryclassPoint {}
這個類目前還沒什麼用,所以讓我們開始新增一些成員。
欄位
欄位宣告會在類上建立一個公共可寫屬性
tsTryclassPoint {x : number;y : number;}constpt = newPoint ();pt .x = 0;pt .y = 0;
與其他地方一樣,型別註解是可選的;如果未指定,將隱式推斷為 any。
欄位也可以有初始化程式;這些程式碼將在類例項化時自動執行
tsTryclassPoint {x = 0;y = 0;}constpt = newPoint ();// Prints 0, 0console .log (`${pt .x }, ${pt .y }`);
就像 const、let 和 var 一樣,類屬性的初始化程式將用於推斷其型別
tsTryconstpt = newPoint ();Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.pt .x = "0";
--strictPropertyInitialization
strictPropertyInitialization 設定控制類欄位是否必須在建構函式中進行初始化。
tsTryclassBadGreeter {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.: string; name }
tsTryclassGoodGreeter {name : string;constructor() {this.name = "hello";}}
請注意,欄位需要在建構函式本身中進行初始化。TypeScript 不會分析你在建構函式中呼叫的方法來檢測初始化,因為派生類可能會重寫這些方法,從而導致成員未能初始化。
如果你打算透過建構函式以外的方式來確切初始化欄位(例如,也許某個外部庫在為你填充部分類),你可以使用明確賦值斷言運算子 !
tsTryclassOKGreeter {// Not initialized, but no errorname !: string;}
readonly
欄位可以使用 readonly 修飾符作為字首。這可以防止在建構函式之外對該欄位進行賦值。
tsTryclassGreeter {readonlyname : string = "world";constructor(otherName ?: string) {if (otherName !==undefined ) {this.name =otherName ;}}err () {this.Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.= "not ok"; name }}constg = newGreeter ();Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.g .= "also not ok"; name
建構函式
背景閱讀
建構函式 (MDN)
類建構函式與函式非常相似。你可以新增帶有型別註解、預設值和過載的引數。
tsTryclassPoint {x : number;y : number;// Normal signature with defaultsconstructor(x = 0,y = 0) {this.x =x ;this.y =y ;}}
tsTryclassPoint {x : number = 0;y : number = 0;// Constructor overloadsconstructor(x : number,y : number);constructor(xy : string);constructor(x : string | number,y : number = 0) {// Code logic here}}
類建構函式簽名與函式簽名之間只有幾點差異:
- 建構函式不能有型別引數——這些屬於外部類宣告,我們稍後會學習。
- 建構函式不能有返回型別註解——返回的始終是類例項型別。
Super 呼叫
正如在 JavaScript 中一樣,如果你有基類,則需要在建構函式體內使用任何 this. 成員之前呼叫 super();。
tsTryclassBase {k = 4;}classDerived extendsBase {constructor() {// Prints a wrong value in ES5; throws exception in ES6'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.console .log (this .k );super();}}
忘記呼叫 super 是 JavaScript 中很容易犯的錯誤,但 TypeScript 會在必要時提醒你。
方法
背景閱讀
方法定義
類上的函式屬性稱為方法。方法可以使用與函式和建構函式完全相同的型別註解。
tsTryclassPoint {x = 10;y = 10;scale (n : number): void {this.x *=n ;this.y *=n ;}}
除了標準的型別註解外,TypeScript 不會為方法新增任何其他新內容。
請注意,在方法體內,仍然必須透過 this. 訪問欄位和其他方法。方法體內未限定的名稱始終指向封閉作用域中的內容。
tsTryletx : number = 0;classC {x : string = "hello";m () {// This is trying to modify 'x' from line 1, not the class propertyType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.= "world"; x }}
Getter / Setter
類也可以有存取器。
tsTryclassC {_length = 0;getlength () {return this._length ;}setlength (value ) {this._length =value ;}}
請注意,在 JavaScript 中,沒有額外邏輯的欄位支援 get/set 對極少有用。如果你不需要在 get/set 操作期間新增額外邏輯,直接暴露公共欄位完全沒問題。
TypeScript 對存取器有一些特殊的推斷規則:
- 如果存在
get但沒有set,則屬性自動為readonly。 - 如果未指定 setter 引數的型別,則會從 getter 的返回型別中推斷得出。
自 TypeScript 4.3 起,可以為 getter 和 setter 設定不同的型別。
tsTryclassThing {_size = 0;getsize (): number {return this._size ;}setsize (value : string | number | boolean) {letnum =Number (value );// Don't allow NaN, Infinity, etcif (!Number .isFinite (num )) {this._size = 0;return;}this._size =num ;}}
索引簽名
類可以宣告索引簽名;它們的工作方式與 其他物件型別的索引簽名 相同。
tsTryclassMyClass {[s : string]: boolean | ((s : string) => boolean);check (s : string) {return this[s ] as boolean;}}
因為索引簽名型別還需要捕獲方法的型別,所以很難有效地使用這些型別。通常最好將索引資料儲存在其他地方,而不是直接儲存在類例項上。
類繼承
像其他具有面向物件特性的語言一樣,JavaScript 中的類可以從基類繼承。
implements 子句
你可以使用 implements 子句來檢查類是否滿足特定的 interface。如果類未能正確實現它,將會報錯。
tsTryinterfacePingable {ping (): void;}classSonar implementsPingable {ping () {console .log ("ping!");}}classClass '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'.implements Ball Pingable {pong () {console .log ("pong!");}}
類也可以實現多個介面,例如 class C implements A, B {。
注意事項
重要的是要明白,implements 子句僅用於檢查該類是否可以被當作介面型別來處理。它根本不會改變類或其方法的型別。一個常見的錯誤是假設 implements 子句會改變類型別——其實不會!
tsTryinterfaceCheckable {check (name : string): boolean;}classNameChecker implementsCheckable {Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.check () { s // Notice no error herereturns .toLowerCase () === "ok";}}
在這個例子中,我們可能期望 s 的型別會受到 check 的 name: string 引數的影響。但事實並非如此——implements 子句不會改變類主體的檢查方式或其型別推斷方式。
同樣,實現一個帶有可選屬性的介面並不會建立該屬性。
tsTryinterfaceA {x : number;y ?: number;}classC implementsA {x = 0;}constc = newC ();Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.c .= 10; y
extends 子句
背景閱讀
extends 關鍵字 (MDN)
類可以 extend(繼承)自基類。派生類擁有其基類的所有屬性和方法,還可以定義額外的成員。
tsTryclassAnimal {move () {console .log ("Moving along!");}}classDog extendsAnimal {woof (times : number) {for (leti = 0;i <times ;i ++) {console .log ("woof!");}}}constd = newDog ();// Base class methodd .move ();// Derived class methodd .woof (3);
重寫方法
背景閱讀
super 關鍵字 (MDN)
派生類也可以重寫基類的欄位或屬性。你可以使用 super. 語法來訪問基類方法。注意,由於 JavaScript 類是一個簡單的查詢物件,因此沒有“super 欄位”的概念。
TypeScript 強制要求派生類始終是其基類的子型別。
例如,以下是一種重寫方法的合法方式
tsTryclassBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {greet (name ?: string) {if (name ===undefined ) {super.greet ();} else {console .log (`Hello, ${name .toUpperCase ()}`);}}}constd = newDerived ();d .greet ();d .greet ("reader");
重要的是,派生類必須遵循其基類的契約。請記住,透過基類引用來引用派生類例項是非常常見的(且總是合法的!)
tsTry// Alias the derived instance through a base class referenceconstb :Base =d ;// No problemb .greet ();
如果 Derived 不遵循 Base 的契約會怎樣?
tsTryclassBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {// Make this parameter requiredProperty '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.( greet name : string) {console .log (`Hello, ${name .toUpperCase ()}`);}}
如果我們忽略錯誤編譯此程式碼,該示例最終會崩潰。
tsTryconstb :Base = newDerived ();// Crashes because "name" will be undefinedb .greet ();
僅型別欄位宣告
當 target >= ES2022 或 useDefineForClassFields 為 true 時,類欄位會在父類建構函式完成後進行初始化,覆蓋父類設定的任何值。當你只想為繼承的欄位重新宣告更精確的型別時,這可能會成為問題。為了處理這些情況,你可以編寫 declare 來向 TypeScript 指示此欄位宣告不應產生執行時效果。
tsTryinterfaceAnimal {dateOfBirth : any;}interfaceDog extendsAnimal {breed : any;}classAnimalHouse {resident :Animal ;constructor(animal :Animal ) {this.resident =animal ;}}classDogHouse extendsAnimalHouse {// Does not emit JavaScript code,// only ensures the types are correctdeclareresident :Dog ;constructor(dog :Dog ) {super(dog );}}
初始化順序
JavaScript 類初始化的順序在某些情況下可能會令人驚訝。讓我們考慮這段程式碼
tsTryclassBase {name = "base";constructor() {console .log ("My name is " + this.name );}}classDerived extendsBase {name = "derived";}// Prints "base", not "derived"constd = newDerived ();
發生了什麼?
JavaScript 定義的類初始化順序如下:
- 基類欄位被初始化
- 基類建構函式執行
- 派生類欄位被初始化
- 派生類建構函式執行
這意味著基類建構函式在其自己的建構函式中看到了它自己的 name 值,因為派生類的欄位初始化尚未執行。
繼承內建型別
注意:如果你不打算繼承內建型別(如
Array、Error、Map等),或者你的編譯目標明確設定為ES6/ES2015或更高版本,你可以跳過此部分。
在 ES2015 中,隱式返回物件的建構函式會用呼叫者 super(...) 的返回值替換 this 的值。生成的建構函式程式碼必須捕獲 super(...) 的任何潛在返回值,並將其替換為 this。
因此,對 Error、Array 等進行子類化可能無法按預期工作。這是因為 Error、Array 等的建構函式使用 ECMAScript 6 的 new.target 來調整原型鏈;然而,在 ECMAScript 5 中呼叫建構函式時,無法確保 new.target 的值。其他向下相容的編譯器預設通常也有同樣的限制。
對於如下的子類:
tsTryclassMsgError extendsError {constructor(m : string) {super(m );}sayHello () {return "hello " + this.message ;}}
你可能會發現:
- 在構造這些子類返回的物件上,方法可能是
undefined,因此呼叫sayHello會導致錯誤。 - 子類例項與它們例項之間的
instanceof將失效,因此(new MsgError()) instanceof MsgError將返回false。
作為建議,你可以在任何 super(...) 呼叫之後立即手動調整原型。
tsTryclassMsgError extendsError {constructor(m : string) {super(m );// Set the prototype explicitly.Object .setPrototypeOf (this,MsgError .prototype );}sayHello () {return "hello " + this.message ;}}
然而,MsgError 的任何子類也必須手動設定原型。對於不支援 Object.setPrototypeOf 的執行時,你或許可以使用 __proto__。
不幸的是,這些變通方法在 Internet Explorer 10 及更早版本上不起作用。你可以手動將原型上的方法複製到例項本身上(即 MsgError.prototype 到 this 上),但原型鏈本身無法修復。
成員可見性
你可以使用 TypeScript 控制某些方法或屬性對類外部程式碼是否可見。
public
類成員的預設可見性是 public。public 成員可以在任何地方被訪問。
tsTryclassGreeter {publicgreet () {console .log ("hi!");}}constg = newGreeter ();g .greet ();
因為 public 已經是預設的可見性修飾符,你不需要在類成員上顯式編寫它,但出於風格/可讀性的考慮,你也可以選擇新增。
protected
protected 成員僅對宣告它們的類的子類可見。
tsTryclassGreeter {publicgreet () {console .log ("Hello, " + this.getName ());}protectedgetName () {return "hi";}}classSpecialGreeter extendsGreeter {publichowdy () {// OK to access protected member hereconsole .log ("Howdy, " + this.getName ());}}constg = newSpecialGreeter ();g .greet (); // OKProperty '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.g .(); getName
暴露 protected 成員
派生類需要遵循其基類的契約,但可以選擇暴露具有更多功能的基類子型別。這包括將 protected 成員變為 public。
tsTryclassBase {protectedm = 10;}classDerived extendsBase {// No modifier, so default is 'public'm = 15;}constd = newDerived ();console .log (d .m ); // OK
請注意,Derived 本來就已經能夠自由讀取和寫入 m,所以這並不會從根本上改變這種情況的“安全性”。這裡主要要注意的是,在派生類中,如果這種暴露不是故意的,我們需要小心重複使用 protected 修飾符。
跨層級 protected 訪問
TypeScript 不允許在類層級結構中訪問兄弟類的 protected 成員。
tsTryclassBase {protectedx : number = 1;}classDerived1 extendsBase {protectedx : number = 5;}classDerived2 extendsBase {f1 (other :Derived2 ) {other .x = 10;}f2 (other :Derived1 ) {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.other .= 10; x }}
這是因為在 Derived2 中訪問 x 應該僅對 Derived2 的子類合法,而 Derived1 並不是其中之一。此外,如果透過 Derived1 引用訪問 x 是非法的(這理所應當是非法的!),那麼透過基類引用訪問它也絕不應該改善這種情況。
另請參閱 為什麼我不能從派生類訪問受保護的成員?,文中對 C# 中關於相同主題的推理進行了更多解釋。
private
private 與 protected 類似,但即便是子類也不允許訪問該成員。
tsTryclassBase {privatex = 0;}constb = newBase ();// Can't access from outside the classProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (b .); x
tsTryclassDerived extendsBase {showX () {// Can't access in subclassesProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (this.); x }}
因為 private 成員對派生類不可見,所以派生類無法提高它們的可見性。
tsTryclassBase {privatex = 0;}classClass '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'.extends Derived Base {x = 1;}
跨例項 private 訪問
不同的面向物件程式語言對於“同一類的不同例項是否可以訪問彼此的 private 成員”意見不一。雖然 Java、C#、C++、Swift 和 PHP 等語言允許這樣做,但 Ruby 不允許。
TypeScript 確實允許跨例項的 private 訪問。
tsTryclassA {privatex = 10;publicsameAs (other :A ) {// No errorreturnother .x === this.x ;}}
注意事項
與其他 TypeScript 型別系統的方面一樣,private 和 protected 僅在型別檢查期間強制執行。
這意味著 JavaScript 執行時構造(如 in 或簡單的屬性查詢)仍然可以訪問 private 或 protected 成員。
tsTryclassMySafe {privatesecretKey = 12345;}
js// In a JavaScript file...const s = new MySafe();// Will print 12345console.log(s.secretKey);
private 在型別檢查期間也允許使用括號表示法進行訪問。這使得 private 宣告的欄位在單元測試等場景下可能更容易訪問,缺點是這些欄位只是軟私有(soft private),並不嚴格強制執行私有性。
tsTryclassMySafe {privatesecretKey = 12345;}consts = newMySafe ();// Not allowed during type checkingProperty 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.console .log (s .); secretKey // OKconsole .log (s ["secretKey"]);
與 TypeScript 的 private 不同,JavaScript 的 私有欄位 (#) 在編譯後仍然保持私有,並且不提供像括號表示法訪問那樣的逃生艙,使它們成為硬私有(hard private)。
tsTryclassDog {#barkAmount = 0;personality = "happy";constructor() {}}
tsTry"use strict";class Dog {#barkAmount = 0;personality = "happy";constructor() { }}
當編譯為 ES2021 或更低版本時,TypeScript 將使用 WeakMap 來代替 #。
tsTry"use strict";var _Dog_barkAmount;class Dog {constructor() {_Dog_barkAmount.set(this, 0);this.personality = "happy";}}_Dog_barkAmount = new WeakMap();
如果你需要保護類中的值免受惡意行為者的侵害,你應該使用提供硬執行時私有性的機制,如閉包、WeakMap 或私有欄位。請注意,這些在執行時新增的隱私檢查可能會影響效能。
靜態成員
背景閱讀
靜態成員 (MDN)
類可以有 static(靜態)成員。這些成員不與類的特定例項關聯。它們可以透過類建構函式物件本身來訪問。
tsTryclassMyClass {staticx = 0;staticprintX () {console .log (MyClass .x );}}console .log (MyClass .x );MyClass .printX ();
靜態成員也可以使用相同的 public、protected 和 private 可見性修飾符。
tsTryclassMyClass {private staticx = 0;}Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.console .log (MyClass .); x
靜態成員也是可以繼承的。
tsTryclassBase {staticgetGreeting () {return "Hello world";}}classDerived extendsBase {myGreeting =Derived .getGreeting ();}
特殊的靜態名稱
重寫 Function 原型上的屬性通常是不安全/不可行的。因為類本身就是可以用 new 呼叫的函式,所以某些 static 名稱不能使用。像 name、length 和 call 這樣的函式屬性不能定義為 static 成員。
tsTryclassS {staticStatic 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'.= "S!"; name }
為什麼沒有靜態類?
TypeScript(和 JavaScript)沒有像 C# 那樣名為 static class 的構造。
這些構造之所以存在,僅僅是因為那些語言強制所有資料和函式都必須在類內部;而由於 TypeScript 中不存在這種限制,因此不需要它們。僅有一個例項的類通常在 JavaScript/TypeScript 中僅表示為一個普通物件。
例如,我們不需要 TypeScript 中的“靜態類”語法,因為普通物件(甚至頂層函式)完全可以勝任這項工作。
tsTry// Unnecessary "static" classclassMyStaticClass {staticdoSomething () {}}// Preferred (alternative 1)functiondoSomething () {}// Preferred (alternative 2)constMyHelperObject = {dosomething () {},};
類中的 static 塊
靜態塊允許你編寫一系列具有自身作用域的語句,這些語句可以訪問包含類中的私有欄位。這意味著我們可以編寫具有所有語句編寫能力的初始化程式碼,不會造成變數洩露,並且可以完全訪問類的內部結構。
tsTryclassFoo {static #count = 0;getcount () {returnFoo .#count;}static {try {constlastInstances =loadLastInstances ();Foo .#count +=lastInstances .length ;}catch {}}}
泛型類
類和介面一樣,可以是泛型的。當泛型類使用 new 例項化時,其型別引數的推斷方式與函式呼叫相同。
tsTryclassBox <Type > {contents :Type ;constructor(value :Type ) {this.contents =value ;}}constb = newBox ("hello!");
類可以使用與介面相同的泛型約束和預設值。
靜態成員中的型別引數
這段程式碼是不合法的,其原因可能並不顯而易見。
tsTryclassBox <Type > {staticStatic members cannot reference class type parameters.2302Static members cannot reference class type parameters.defaultValue :; Type }
記住,型別始終會被完全擦除!在執行時,只有一個 Box.defaultValue 屬性槽。這意味著設定 Box<string>.defaultValue(如果這可行的話)也會改變 Box<number>.defaultValue ——這不好。泛型類的 static 成員永遠無法引用類的型別引數。
類中執行時的 this
背景閱讀
this 關鍵字 (MDN)
重要的是要記住,TypeScript 不會改變 JavaScript 的執行時行為,而 JavaScript 以具有某些獨特的執行時行為而聞名。
JavaScript 對 this 的處理確實非常特殊。
tsTryclassMyClass {name = "MyClass";getName () {return this.name ;}}constc = newMyClass ();constobj = {name : "obj",getName :c .getName ,};// Prints "obj", not "MyClass"console .log (obj .getName ());
簡而言之,預設情況下,函式內部 this 的值取決於函式是如何被呼叫的。在此示例中,因為函式是透過 obj 引用呼叫的,所以其 this 的值是 obj,而不是類例項。
這很少是你想要的結果!TypeScript 提供了一些方法來減輕或防止這類錯誤。
箭頭函式
背景閱讀
箭頭函式 (MDN)
如果你有一個函式經常會被呼叫且會丟失其 this 上下文,那麼使用箭頭函式屬性而不是方法定義是有意義的。
tsTryclassMyClass {name = "MyClass";getName = () => {return this.name ;};}constc = newMyClass ();constg =c .getName ;// Prints "MyClass" instead of crashingconsole .log (g ());
這有一些權衡:
- 即使對於未經過 TypeScript 檢查的程式碼,
this值在執行時也保證是正確的。 - 這會消耗更多記憶體,因為每個類例項都會擁有以這種方式定義的每個函式的副本。
- 你不能在派生類中使用
super.getName,因為原型鏈中沒有條目可以從中獲取基類方法。
this 引數
在方法或函式定義中,名為 this 的初始引數在 TypeScript 中具有特殊含義。這些引數在編譯期間會被擦除。
tsTry// TypeScript input with 'this' parameterfunctionfn (this :SomeType ,x : number) {/* ... */}
js// JavaScript outputfunction fn(x) {/* ... */}
TypeScript 會檢查呼叫帶有 this 引數的函式時是否具有正確的上下文。與其使用箭頭函式,我們可以在方法定義中新增一個 this 引數,以靜態強制該方法被正確呼叫。
tsTryclassMyClass {name = "MyClass";getName (this :MyClass ) {return this.name ;}}constc = newMyClass ();// OKc .getName ();// Error, would crashconstg =c .getName ;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'.console .log (g ());
這種方法與箭頭函式方法存在相反的權衡:
- JavaScript 呼叫者可能仍然會不經意地錯誤使用類方法。
- 每個類定義只會分配一個函式,而不是每個類例項分配一個。
- 基方法定義仍然可以透過
super呼叫。
this 型別
在類中,一種名為 this 的特殊型別會動態引用當前類的型別。讓我們看看這有何用處。
tsTryclassBox {contents : string = "";set (value : string) {this.contents =value ;return this;}}
在這裡,TypeScript 將 set 的返回型別推斷為 this,而不是 Box。現在讓我們建立一個 Box 的子類。
tsTryclassClearableBox extendsBox {clear () {this.contents = "";}}consta = newClearableBox ();constb =a .set ("hello");
你也可以在引數型別註解中使用 this。
tsTryclassBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}
這與編寫 other: Box 不同——如果你有派生類,其 sameAs 方法現在將僅接受該相同派生類的其他例項。
tsTryclassBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}classDerivedBox extendsBox {otherContent : string = "?";}constbase = newBox ();constderived = newDerivedBox ();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'.derived .sameAs (); base
基於 this 的型別保護
你可以在類和介面的方法返回位置使用 this is Type。當與型別收窄(例如 if 語句)混合使用時,目標物件的型別將被收窄為指定的 Type。
tsTryclassFileSystemObject {isFile (): this isFileRep {return this instanceofFileRep ;}isDirectory (): this isDirectory {return this instanceofDirectory ;}isNetworked (): this isNetworked & this {return this.networked ;}constructor(publicpath : string, privatenetworked : boolean) {}}classFileRep extendsFileSystemObject {constructor(path : string, publiccontent : string) {super(path , false);}}classDirectory extendsFileSystemObject {children :FileSystemObject [];}interfaceNetworked {host : string;}constfso :FileSystemObject = newFileRep ("foo/bar.txt", "foo");if (fso .isFile ()) {fso .content ;} else if (fso .isDirectory ()) {fso .children ;} else if (fso .isNetworked ()) {fso .host ;}
基於 this 的型別保護的一個常見用例是允許對特定欄位進行懶驗證。例如,當 hasValue 被驗證為 true 時,此用例會從 box 內持有的值中移除一個 undefined。
tsTryclassBox <T > {value ?:T ;hasValue (): this is {value :T } {return this.value !==undefined ;}}constbox = newBox <string>();box .value = "Gameboy";box .value ;if (box .hasValue ()) {box .value ;}
引數屬性
TypeScript 提供了特殊的語法,將建構函式引數轉換為具有相同名稱和值的類屬性。這些被稱為引數屬性,透過在建構函式引數前新增可見性修飾符 public、private、protected 或 readonly 來建立。生成的欄位將獲得這些修飾符。
tsTryclassParams {constructor(public readonlyx : number,protectedy : number,privatez : number) {// No body necessary}}consta = newParams (1, 2, 3);console .log (a .x );Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.console .log (a .); z
類表示式
背景閱讀
類表示式 (MDN)
類表示式與類宣告非常相似。唯一的真正區別是類表示式不需要名稱,儘管我們可以透過它們繫結到的任何識別符號來引用它們。
tsTryconstsomeClass = class<Type > {content :Type ;constructor(value :Type ) {this.content =value ;}};constm = newsomeClass ("Hello, world");
建構函式簽名
JavaScript 類是用 new 運算子例項化的。給定類本身的型別,InstanceType 工具型別可以模擬此操作。
tsTryclassPoint {createdAt : number;x : number;y : numberconstructor(x : number,y : number) {this.createdAt =Date .now ()this.x =x ;this.y =y ;}}typePointInstance =InstanceType <typeofPoint >functionmoveRight (point :PointInstance ) {point .x += 5;}constpoint = newPoint (3, 4);moveRight (point );point .x ; // => 8
abstract 類和成員
TypeScript 中的類、方法和欄位可以是 abstract(抽象的)。
抽象方法 或 抽象欄位 是尚未提供實現的方法或欄位。這些成員必須存在於 抽象類 中,該類不能直接例項化。
抽象類的作用是作為實現所有抽象成員的子類的基類。當一個類沒有任何抽象成員時,它被稱為 具體類(concrete)。
讓我們看一個例子。
tsTryabstract classBase {abstractgetName (): string;printName () {console .log ("Hello, " + this.getName ());}}constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.b = newBase ();
我們不能用 new 例項化 Base,因為它是抽象的。相反,我們需要建立一個派生類並實現抽象成員。
tsTryclassDerived extendsBase {getName () {return "world";}}constd = newDerived ();d .printName ();
請注意,如果我們忘記實現基類的抽象成員,我們將收到一個錯誤。
tsTryclassNon-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'.extends Derived Base {// forgot to do anything}
抽象構造簽名
有時你想要接受某種產生從特定抽象類派生的例項的類建構函式。
例如,你可能想要編寫這段程式碼。
tsTryfunctiongreet (ctor : typeofBase ) {constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.instance = newctor ();instance .printName ();}
TypeScript 正確地告訴你,你正試圖例項化一個抽象類。畢竟,鑑於 greet 的定義,編寫此程式碼是完全合法的,但這最終會構建一個抽象類。
tsTry// Bad!greet (Base );
相反,你需要編寫一個接受具有構造簽名的內容的函式。
tsTryfunctiongreet (ctor : new () =>Base ) {constinstance = newctor ();instance .printName ();}greet (Derived );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.greet (); Base
現在 TypeScript 正確地告訴你哪些類建構函式可以被呼叫——Derived 可以,因為它是一個具體類,但 Base 不能。
類之間的關係
在大多數情況下,TypeScript 中的類是結構化比較的,與其他型別相同。
例如,這兩個類可以相互替換,因為它們是相同的。
tsTryclassPoint1 {x = 0;y = 0;}classPoint2 {x = 0;y = 0;}// OKconstp :Point1 = newPoint2 ();
類似地,即使沒有顯式繼承,類之間也存在子型別關係。
tsTryclassPerson {name : string;age : number;}classEmployee {name : string;age : number;salary : number;}// OKconstp :Person = newEmployee ();
這聽起來很簡單,但有些情況看起來比其他情況更奇怪。
空類沒有成員。在結構化型別系統中,沒有成員的型別通常是任何其他型別的超型別。因此,如果你編寫一個空類(別這麼做!),任何內容都可以用來替換它。
tsTryclassEmpty {}functionfn (x :Empty ) {// can't do anything with 'x', so I won't}// All OK!fn (window );fn ({});fn (fn );