- 說明:目前網上沒有 TypeScript 最新官方文件的中文翻譯,所以有了這麼一個翻譯計劃。因為我也是 TypeScript 的初學者,所以無法保證翻譯百分之百準確,若有錯誤,歡迎評論區指出;
- 翻譯內容:暫定翻譯內容為 TypeScript Handbook,後續有空會補充翻譯文件的其它部分;
- 專案地址:TypeScript-Doc-Zh,如果對你有幫助,可以點一個 star ~
本章節官方文件地址:Classes
背景導讀:類(MDN)
類
TypeScript 為 ES2015 引入的 class
關鍵字提供了全面的支援。
就像其它的 JavaScript 語言特性一樣,TypeScript 也為類提供了型別註解和其它語法,以幫助開發者表示類和其它型別之間的關係。
類成員
這是一個最基本的類 —— 它是空的:
class Point {}
這個類目前沒有什麼用,所以我們給它新增一些成員吧。
欄位
宣告欄位相當於是給類新增了一個公共的、可寫的屬性:
class Point {
x: number;
y: number;
}
const pt = new Point()
pt.x = 0;
pt.y = 0;
和其它特性一樣,這裡的型別註解也是可選的,但如果沒有指定型別,則會隱式採用 any
型別。
欄位也可以進行初始化,初始化過程會在類例項化的時候自動進行:
class Point {
x = 0;
y = 0;
}
const pt = new Point();
// 列印 0, 0
console.log(`${pt.x}, ${pt.y}`);
就像使用 const
、let
和 var
一樣,類屬性的初始化語句也會被用於進行型別推斷:
const pt = new Point();
pt.x = "0";
// Type 'string' is not assignable to type 'number'.
--strictPropertyInitialization
配置項 strictPropertyInitialization 用於控制類的欄位是否需要在構造器中進行初始化。
class BadGreeter {
name: string;
^
// Property 'name' has no initializer and is not definitely assigned in the constructor.
}
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
注意,欄位需要在構造器自身內部進行初始化。TypeScript 不會分析在構造器中呼叫的方法以檢測初始化語句,因為派生類可能會重寫這些方法,導致初始化成員失敗。
如果你堅持要使用除了構造器之外的方法(比如使用一個外部庫填充類的內容)去初始化一個欄位,那麼你可以使用確定賦值斷言運算子 !
:
class OKGreeter {
// 沒有初始化,但不會報錯
name!: string;
}
readonly
欄位可以加上 readonly
修飾符作為字首,以防止在構造器外面對欄位進行賦值。
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.
}
}
const g = new Greeter();
g.name = "also not ok";
^
// Cannot assign to 'name' because it is a read-only property.
構造器
類的構造器和函式很像,你可以給它的引數新增型別註解,可以使用引數預設值或者是函式過載:
class Point {
x: number;
y: number;
// 使用了引數預設值的正常簽名
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Point {
// 使用過載
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
類的構造器簽名和函式簽名只有一點區別:
- 構造器不能使用型別引數 —— 型別引數屬於類宣告的部分,稍後我們會進行學習
- 構造器不能給返回值新增型別註解 —— 它返回的型別始終是類例項的型別
super
呼叫
和 JavaScript 一樣,如果你有一個基類和一個派生類,那麼在派生類中使用 this.
訪問類成員之前,必須先在構造器中呼叫 super();
:
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// ES5 下列印出錯誤的值,ES6 下報錯
console.log(this.k);
^
// 'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
在 JavaScript 中,忘記呼叫 super
是一個常見的錯誤,但 TypeScript 會在必要時給你提醒。
方法
類的屬性可能是一個函式,這時候我們稱其為方法。方法和函式以及構造器一樣,也可以使用各種型別註解:
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
除了標準的型別註解之外,TypeScript 沒有給方法新增什麼新的東西。
注意,在方法體中,必須通過 this.
才能訪問到類的欄位和其它方法。在方法體中使用不合規的名字,將會被視為是在訪問鄰近作用域中的變數:
let x: number = 0;
class C {
x: string = "hello";
m() {
// 下面這句是在試圖修改第一行的 x,而不是類的屬性
x = "world";
^
// Type 'string' is not assignable to type 'number'.
}
}
Getters/Setters
類也可以有訪問器:
class C {
_length = 0;
get length(){
return this._length;
}
set length(value){
this._length = value;
}
}
注意:在 JavaScript 中,一個沒有額外邏輯的 get/set 對是沒有什麼作用的。如果在執行 get/set 操作的時候不需要新增額外的邏輯,那麼只需要將欄位暴露為公共欄位即可。
對於訪問器,TypeScript 有一些特殊的推斷規則:
- 如果
get
存在而set
不存在,那麼屬性會自動成為只讀屬性 - 如果沒有指定 setter 引數的型別,那麼會基於 getter 返回值的型別去推斷引數型別
- getter 和 setter 必須具備相同的成員可見性。
從 TypeScript 4.3 開始,訪問器的 getter 和 setter 可以使用不同的型別。
class Thing {
_size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// 不允許使用 NaN、Infinity 等
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
索引簽名
類可以宣告索引簽名,其工作方式和其它物件型別的索引簽名一樣:
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
因為索引簽名型別也需要捕獲方法的型別,所以要有效地使用這些型別並不容易。通常情況下,最好將索引資料儲存在另一個位置,而不是類例項本身。
類繼承
和其它面嚮物件語言一樣,JavaScript 中的類可以繼承自基類。
implements
子句
你可以使用一個 implements
子句去檢查類是否符合某個特定的介面。如果類沒有正確地實現這個介面,那麼就會丟擲一個錯誤:
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'.
*/
pong() {
console.log("pong!");
}
}
類可以實現多個介面,比如 class C implements A,B {
。
注意事項
有個要點需要理解,那就是 implements
子句只是用於檢查類是否可以被視為某個介面型別,它完全不會改變類的型別或者它的方法。常見的錯誤是認為 implements
子句會改變類的型別 —— 實際上是不會的!
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) {
^
//Parameter 's' implicitly has an 'any' type.
// 注意這裡不會丟擲錯誤
return s.toLowercse() === "ok";
^
// any
}
}
在這個例子中,我們可能會認為 s
的型別會受到介面中 check
的 name: string
引數的影響。但實際上不會 —— implements
子句不會對類內容體的檢查以及型別推斷產生任何影響。
同理,實現一個帶有可選屬性的介面,並不會建立該屬性:
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'.
extends
子句
類可以繼承自某個基類。派生類擁有基類的所有屬性和方法,同時也可以定義額外的成員。
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();
// 基類方法
d.move();
// 派生類方法
d.woof(3);
重寫方法
派生類也可以重寫基類的欄位或者屬性。你可以使用 super.
語法訪問基類的方法。注意,由於 JavaScript 的類只是一個簡單的查詢物件,所以不存在“父類欄位”的概念。
TypeScript 強制認為派生類總是基類的一個子類。
比如,下面是一個合法的重寫方法的例子:
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");
很重要的一點是,派生類會遵循基類的約束。通過一個基類引用去引用一個派生類,是很常見(並且總是合法的!)的一種做法:
// 通過一個基類引用去命名一個派生類例項
const b: Base = d;
// 沒有問題
b.greet();
如果派生類 Derived
沒有遵循基類 Base
的約束,會怎麼樣呢?
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// 讓這個引數成為必選引數
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'.
*/
console.log(`Hello, ${name.toUpperCase()}`);
}
}
如果無視錯誤並編譯程式碼,那麼下面的程式碼執行後會報錯:
const b: Base = new Derived();
// 因為 name 是 undefined,所以報錯
b.greet();
初始化順序
JavaScript 類的初始化順序在某些情況下可能會讓你感到意外。我們看看下面的程式碼:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// 列印 base 而不是 derived
const d = new Derived();
這裡發生了什麼事呢?
根據 JavaScript 的定義,類初始化的順序是:
- 初始化基類的欄位
- 執行基類的構造器
- 初始化派生類的欄位
- 執行派生類的構造器
這意味著,因為基類構造器執行的時候派生類的欄位尚未進行初始化,所以基類構造器只能看到自己的 name
值。
繼承內建型別
注意:如果你不打算繼承諸如 Array、Error、Map 等內建型別,或者你的編譯目標顯式設定為 ES6/ES2015 或者更高的版本,那麼你可以跳過這部分的內容。
在 ES2015 中,返回例項物件的構造器會隱式地將 this
的值替換為 super(...)
的任意呼叫者。有必要讓生成的構造器程式碼捕獲 super(...)
的任意潛在的返回值,並用 this
替換它。
因此,Error
、Array
等的子類可能無法如預期那樣生效。這是因為諸如 Error
、Array
這樣的建構函式使用了 ES6 的 new.target
去調整原型鏈,但是,在 ES5 中呼叫構造器函式的時候,沒有類似的方法可以確保 new.target
的值。預設情況下,其它底層編譯器通常也具有相同的限制。
對於一個像下面這樣的子類:
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
你可能會發現:
- 呼叫子類之後返回的例項物件,其方法可能是
undefined
,所以呼叫sayHello
將會丟擲錯誤 - 子類例項和子類之間的
instanceof
可能被破壞,所以(new MsgError()) instanceof MsgError
將會返回false
。
推薦的做法是,在任意的 super(...)
呼叫後面手動地調整原型鏈:
class MsgError extends Error {
constructor(m: string) {
super(m);
// 顯式設定原型鏈
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
return "hello " + this.message;
}
}
不過,MsgError
的任意子類也需要手動設定原型。對於不支援 Object.setPrototypeOf 的執行時,你可以改用 __proto__
。
糟糕的是,這些變通方法在 IE10 或者更舊的版本上無法使用.aspx)。你可以手動將原型上的方法複製到例項上(比如將 MsgError.prototype
的方法複製給 this
),但原型鏈本身無法被修復。
成員可見性
你可以使用 TypeScript 控制特定的方法或屬性是否在類的外面可見。
public
類成員的預設可見性是公有的(public
)。公有成員隨處可以訪問:
class Greeter {
public greet(){
console.log('hi!');
}
}
const g = new Greeter();
g.greet();
由於成員的可見性預設就是公有的,所以你不需要在類成員前面進行顯式宣告,但出於程式碼規範或者可讀性的考慮,你也可以這麼做。
protected
受保護(protected
)成員只在類的子類中可見。
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// 這裡可以訪問受保護成員
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.
公開受保護成員
派生類需要遵循其基類的約束,但可以選擇公開具有更多功能的基類的子類。這包括了讓受保護成員變成公有成員:
class Base {
protected m = 10;
}
class Derived extends Base {
// 沒有修飾符,所以預設可見性是公有的
m = 15;
}
const d = new Dervied();
console.log(d.m); // OK
注意 Dervied
已經可以自由讀寫成員 m
了,所以這麼寫並不會改變這種情況的“安全性”。這裡需要注意的要點是,在派生類中,如果我們無意公開其成員,那麼需要新增 protected
修飾符。
跨層級訪問受保護成員
對於通過一個基類引用訪問受保護成員是否合法,不同的 OOP 語言之間存在爭議:
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: Base) {
other.x = 10;
^
// Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.
}
}
舉個例子,Java 認為上述程式碼是合法的,但 C# 和 C++ 則認為上述程式碼是不合法的。
TypeScript 也認為這是不合法的,因為只有在 Derived2
的子類中訪問 Derived2
的 x
才是合法的,但 Derived1
並不是 Derived2
的子類。而且,如果通過 Derived1
引用訪問 x
就已經是不合法的了(這確實應該是不合法的!),那麼通過基類引用訪問它也同樣應該是不合法的。
關於 C# 為什麼會認為這段程式碼是不合法的,可以閱讀這篇文章瞭解更多資訊:為什麼我無法在一個派生類中去訪問一個受保護成員?
private
private
和 protected
一樣,但宣告瞭 private
的私有成員即使在子類中也無法被訪問到:
class Base {
private x = 0;
}
const b = new Base();
// 無法在類外面訪問
console.log(b.x);
// Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
showX() {
// 無法在子類中訪問
console.log(this.x);
^
// Property 'x' is private and only accessible within class 'Base'.
}
}
由於私有成員對派生類不可見,所以派生類無法提高其可見性:
class Base {
private x = 0;
}
class Dervied extends Base {
/*
Class 'Derived' incorrectly extends base class 'Base'.
Property 'x' is private in type 'Base' but not in type 'Derived'.
*/
x = 1;
}
跨例項訪問私有成員
對於同一個類的不同例項互相訪問對方的私有成員是否合法,不同的 OOP 語言之間存在爭議。Java、C#、C++、Swift 和 PHP 允許這麼做,但 Ruby 則認為這樣做是不合法的。
TypeScript 允許跨例項訪問私有成員:
class A {
private x = 10;
public sameAs(other: A) {
// 不會報錯
return other.x === this.x;
}
}
注意事項
和 TypeScript 型別系統中的其它東西一樣,private
和 protected
只在型別檢查期間生效。
這意味著 JavaScript 執行時的一些操作,諸如 in
或者簡單的屬性查詢仍然可以訪問私有成員或者受保護成員:
class MySafe {
private serectKey = 123345;
}
// 在 JavaScript 檔案中會列印 12345
const s = new MySafe();
console.log(s.secretKey);
而即使是在型別檢查期間,我們也可以通過方括號語法去訪問私有成員。因此,在進行諸如單元測試這樣的操作時,訪問私有欄位會比較容易,但缺點就是這些欄位是“弱私有的”,無法保證嚴格意義上的私有性。
class MySafe {
private secretKey = 12345;
}
const s = new MySafe();
// 在型別檢查期間,不允許這樣訪問私有成員
console.log(s.secretKey);
^
// Property 'secretKey' is private and only accessible within class 'MySafe'.
// 但是可以通過方括號語法訪問
console.log(s["secretKey"]);
和 TypeScript 用 private
宣告的私有成員不同,JavaScript 用 #
宣告的私有欄位在編譯之後也仍然是私有的,並且沒有提供像上面那樣的方括號語法用於訪問私有成員,所以 JavaScript 的私有成員是“強私有的”。
class Dog {
#barkAmount = 0;
personality = 'happy';
constructor() {}
}
以下面這段 TypeScript 程式碼為例:
"use strict";
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() { }
}
把它編譯為 ES2021 或者更低版本的程式碼之後,TypeScript 會使用 WeakMap 代替 #
。
"use strict";
var _Dog_barkAmount;
class Dog {
constructor() {
_Dog_barkAmount.set(this, 0);
this.personality = "happy";
}
}
_Dog_barkAmount = new WeakMap();
如果你需要保護類中的值不被惡意修改,那麼你應該使用提供了執行時私有性保障的機制,比如閉包、WeakMap 或者私有欄位等。注意,這些在執行時新增的私有性檢查可能會影響效能。
靜態成員
背景導讀:靜態成員(MDN)
類可以擁有靜態(static
)成員。這些成員和類的特定例項無關,我們可以通過類構造器物件本身訪問到它們:
class MyClass {
static x = 0;
static printX(){
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
靜態成員也可以使用 public
、protected
和 private
等可見性修飾符:
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
^
// Property 'x' is private and only accessible within class 'MyClass'.
靜態成員也可以被繼承:
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
特殊的靜態成員名字
重寫 Function
原型的屬性通常是不安全/不可能的。因為類本身也是一個可以通過 new
呼叫的函式,所以無法使用一些特定的靜態成員名字。諸如 name
、length
和 call
這樣的函式屬性無法作為靜態成員的名字:
class S {
static name = 'S!';
^
// Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
為什麼沒有靜態類?
TypeScript(和 JavaScript)並沒有像 C# 和 Java 那樣提供靜態類這種結構。
C# 和 Java 之所以需要靜態類,是因為這些語言要求所有的資料和函式必須放在一個類中。因為在 TypeScirpt 中不存在這個限制,所以也就不需要靜態類。只擁有單個例項的類在 JavaScript/TypeScirpt 中通常用一個普通物件表示。
舉個例子,在 TypeScript 中我們不需要“靜態類”語法,因為一個常規的物件(甚至是頂層函式)也可以完成相同的工作:
// 不必要的靜態類
class MyStaticClass {
static doSomething() {}
}
// 首選(方案一)
function doSomething() {}
// 首選(方案二)
const MyHelperObject = {
dosomething() {},
};
類中的靜態塊
靜態塊允許你編寫一系列宣告語句,它們擁有自己的作用域,並且可以訪問包含類中的私有欄位。這意味著我們能夠編寫初始化程式碼,這些程式碼包含了宣告語句,不會有變數洩漏的問題,並且完全可以訪問類的內部。
class Foo {
static #count = 0;
get count(){
return Foo.#count;
}
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
泛型類
類和介面一樣,也可以使用泛型。當用 new
例項化一個泛型類的時候,它的型別引數就像在函式呼叫中那樣被推斷出來:
class Box<Type> {
contents: Type;
constructor(value: Type){
this.contents = value;
}
}
const b = new Box('hello!');
^
// const b: Box<string>
類可以像介面那樣使用泛型約束和預設值。
靜態成員中的型別引數
下面的程式碼是不合法的,但原因可能不那麼明顯:
class Box<Type> {
static defaultValue: Type;
^
// Static members cannot reference class type parameters.
}
記住,型別在編譯後總是會被完全抹除的!在執行時,只有一個 Box.defaultValue
屬性插槽。這意味著設定 Box<string>.defaultValue
(如果可以設定的話)也會改變 Box<number>.defaultValue
—— 這是不行的。泛型類的靜態成員永遠都不能引用類的型別引數。
類的執行時 this
有個要點需要記住,那就是 TypeScript 不會改變 JavaScript 的執行時行為。而眾所周知,JavaScript 擁有一些特殊的執行時行為。
JavaScript 對於 this
的處理確實是很不尋常:
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// 列印 "obj" 而不是 "MyClass"
console.log(obj.getName());
長話短說,預設情況下,函式中 this
的值取決於函式是如何被呼叫的。在這個例子中,由於我們通過 obj
引用去呼叫函式,所以它的 this
的值是 obj
,而不是類例項。
這通常不是我們期望的結果!TypeScript 提供了一些方法讓我們可以減少或者防止這種錯誤的發生。
箭頭函式
如果你的函式在被呼叫的時候經常會丟失 this
上下文,那麼最好使用箭頭函式屬性,而不是方法定義:
class MyClass {
name = 'MyClass';
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// 列印 MyClass
console.log(g());
這種做法有一些利弊權衡:
- 在執行時可以保證
this
的值是正確的,即使對於那些沒有使用 TypeScript 進行檢查的程式碼也是如此 - 這樣會佔用更多記憶體,因為以這種方式定義的函式,會導致每個類例項都有一份函式副本
- 你無法在派生類中使用
super.getName
,因為在原型鏈上沒有入口可以去獲取基類的方法
this
引數
在 TypeScript 的方法或者函式定義中,第一個引數的名字如果是 this
,那麼它有特殊的含義。這樣的引數在編譯期間會被抹除:
// TypeScript 接受 this 引數
function fn(this: SomeType, x: number) {
/* ... */
}
// 輸出得 JavaScript
function fn(x) {
/* ... */
}
TypeScript 會檢查傳入 this
引數的函式呼叫是否位於正確的上下文中。這裡我們沒有使用箭頭函式,而是給方法定義新增了一個 this
引數,以靜態的方式確保方法可以被正確呼叫:
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
// 報錯
const g = c.getName;
console.log(g());
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
這種方法的利弊權衡和上面使用箭頭函式的方法相反:
- JavaScript 的呼叫方可能仍然會在沒有意識的情況下錯誤地呼叫類方法
- 只會給每個類定義分配一個函式,而不是給每個類例項分配一個函式
- 仍然可以通過
super
呼叫基類定義的方法
this
型別
在類中,名為 this
的特殊型別可以動態地引用當前類的型別。我們看一下它是怎麼發揮作用的:
class Box {
contents: string = "";
set(value: string){
^
// (method) Box.set(value: string): this
this.contents = value;
return this;
}
}
這裡,TypeScript 將 set
的返回值型別推斷為 this
,而不是 Box
。現在我們來建立一個 Box
的子類:
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox();
const b = a.set("hello");
^
// const b: ClearableBox
你也可以在引數的型別註解中使用 this
:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
這和使用 other: Box
是不一樣的 —— 如果你有一個派生類,那麼它的 sameAs
方法將只會接受該派生類的其它例項:
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'.
*/
基於 this
的型別保護
你可以在類和介面的方法的返回值型別註解處使用 this is Type
。該語句和型別收縮(比如說 if
語句)一起使用的時候,目標物件的型別會被收縮為指定的 Type
。
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
}
基於 this
的型別保護的常見用例是允許特定欄位的延遲驗證。以下面的程式碼為例,當 hasValue
被驗證為 true 的時候,可以移除 Box
中為 undefined
的 value
值:
class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box();
box.value = "Gameboy";
box.value;
^
// (property) Box<unknown>.value?: unknown
if (box.hasValue()) {
box.value;
^
// (property) value: unknown
}
引數屬性
TypeScript 提供了一種特殊的語法,可以將構造器引數轉化為具有相同名字和值的類屬性。這種語法叫做引數屬性,實現方式是在構造器引數前面加上 public
、private
、protected
或者 readonly
等其中一種可見性修飾符作為字首。最終的欄位將會獲得這些修飾符:
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// 沒有必要編寫構造器的函式體
}
}
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'.
類表示式
背景導讀:類表示式(MDN)
類表示式和類宣告非常相似。唯一的不同在於,類表示式不需要名字,但我們仍然可以通過任意繫結給類表示式的識別符號去引用它們:
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
const m = new someClass("Hello, world");
^
// const m: someClass<string>
抽象類和成員
在 TypeScript 中,類、方法和欄位可能是抽象的。
抽象方法或者抽象欄位在類中沒有對應的實現。這些成員必須存在於一個無法直接被例項化的抽象類中。
抽象類的角色是充當一個基類,讓其子類去實現所有的抽象成員。當一個類沒有任何抽象成員的時候,我們就說它是具體的。
來看一個例子:
abstract class Base {
abstract getName(): string;
printName(){
console.log("Hello, " + this.getName());
}
}
const b = new Base();
// Cannot create an instance of an abstract class.
因為 Base
是一個抽象類,所以我們不能使用 new
去例項化它。相反地,我們需要建立一個派生類,讓它去實現抽象成員:
class Derived extends Base {
getName() {
rteurn "world";
}
}
const d = new Derived();
d.printName();
注意,如果我們忘記實現基類的抽象成員,那麼會丟擲一個錯誤:
class Derived extends Base {
^
// Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
// 忘記實現抽象成員
}
抽象構造簽名
有時候你想要接受一個類構造器函式作為引數,讓它產生某個類的例項,並且這個類是從某個抽象類派生過來的。
舉個例子,你可能想要編寫下面這樣的程式碼:
function greet(ctor: typeof Base) {
const instance = new ctor();
// Cannot create an instance of an abstract class.
instance.printName();
}
TypeScript 會正確地告訴你,你正試圖例項化一個抽象類。畢竟,根據 greet
的定義,編寫這樣的程式碼理應是完全合法的,它最終會構造一個抽象類的例項:
// 不行!
greet(Base);
但它實際上會報錯。所以,你編寫的函式所接受的引數應該帶有一個構造簽名:
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.
*/
現在 TypeScript 可以正確地告知你哪個類構造器函式可以被呼叫了 —— Derived
可以被呼叫,因為它是一個具體類,而 Base
不能被呼叫,因為它是一個抽象類。
類之間的聯絡
在大多數情況下,TypeScript 中的類是在結構上進行比較的,就跟其它型別一樣。
舉個例子,下面這兩個類可以互相替代對方,因為它們在結構上是一模一樣的:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();
類似地,即使沒有顯式宣告繼承關係,類和類之間也可以存在子類聯絡:
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();
這聽起來很簡單易懂,但還有一些情況會比較奇怪。
空類沒有成員。在一個結構化的型別系統中,一個沒有成員的型別通常是任何其它型別的超類。所以如果你編寫了一個空類(不要這麼做!),那麼你可以用任何型別去替代它:
class Empty {}
function fn(x: Empty) {
// 無法對 x 執行任何操作,所以不建議這麼寫
}
// 這些引數都是可以傳入的!
fn(window);
fn({});
fn(fn);