一。this 也是一種類型!
class BasicDOMNode {
constructor(private el: Element) { }
addClass(cssClass: string) {
this.el.classList.add(cssClass);
return this;
}
}
class DOMNode extends BasicDOMNode {
addClasses(cssClasses: string[]) {
for (let cssClass of cssClasses) {
this.addClass(cssClass);
}
return this;
}
}
其中,addClass 與 addClasses 的類型簽名分別是:
addClass(cssClass: string): this
addClasses(cssClasses: string[]): this
返回類型是 this,表示所屬類或接口的子類型(稱之為 有界多態性(F-bounded polymorphism)),例如:
let node = new DOMNode(document.querySelector('div'));
node
.addClass('page')
.addClasses(['active', 'spring'])
.addClass('first')
上面的鏈式調用中,this 類型能夠自動對應到所屬類實例類型上。沒錯,這種 JavaScript 運行時特性,在 TypeScript 靜態類型系統中同樣支持
具體地,TypeScript 中的 this 類型分為 2 類:
-
class this type:類/接口(的成員方法)中的 this 類型
-
function this type:普通函數中的 this 類型
二。Class this type
JavaScript Class 中的 this
// JavaScript
class A {
foo() { return this }
}
class B extends A {
bar() { return this }
}
new B().foo().bar();
上例中的鏈式調用會正常執行,最後返回 B 類實例。我們知道運行時 this 指向當前類或其子類實例,這在 JavaScript 運行時是一種非常常見的行為
也就是說,this 的類型並不是固定的,取決於其調用上下文,例如:
// A 類實例類型
new A().foo();
// B 類實例類型
new B().foo();
// B 類實例類型
new A().foo.call(new B());
Class A 中的 this 並不總是指向 A 類實例(也有可能是 A 的子類實例),那麼,應該如何描述 this 的類型?
this 的類型
要給最初的場景添上類型描述的話,我們可能會這樣嘗試(如果沒有 class this type):
declare class A {
foo(): A;
}
declare class B extends A {
bar(): B;
}
// 錯誤 Property 'bar' does not exist on type 'A'.
new B().foo().bar();
意料之中的結果,foo(): A 返回 A 類實例,當然找不到子類 B 的成員方法。實際期望的是:
A 類實例類型,具有 foo() 方法
|
new B().foo().bar()
|
B 類實例類型,具有 bar() 方法
那麼,進一步嘗試:
declare class A {
foo(): A & B;
}
declare class B extends A {
bar(): B & A;
}
new B().foo().bar();
B 類中的 this 既是 B 類實例也是 A 類實例,姑且認為 bar(): B & A 是合適的,但無論如何 foo(): A & B 是不合理的,因為基類實例並不一定是子類實例……我們似乎沒有辦法給 this 標出一個合適的類型,尤其是在 superThis.subMethod() 的場景
因此,針對類似的場景,有必要引入一種特殊的類型,即 this 類型:
Within a class this would denote a type that behaves like a subtype of the containing class (effectively like a type parameter with the current class as a constraint).
this 類型表現為所屬類/接口的子類型,這與 JavaScript 運行時的 this 值機制一致,例如:
class A {
foo(): this { return this }
}
class B extends A {
bar(): this { return this }
}
new B().foo().bar()
也就是說,this 類型就是 this 值的類型:
In a non-static member of a class or interface, this in a type position refers to the type of this.
實現原理
The polymorphic this type is implemented by providing every class and interface with an implied type parameter that is constrained to the containing type itself.
簡言之,就是把類/接口看作具有隱式類型參數 this 的泛型,並加上其所在類/接口相關的類型約束
Consider every class/interface as a generic type with an implicit this type arguments. The this type parameter is constrained to the type, i.e.
A<this extends A<A>>. The type of the value this inside a class or an interface is the generic type parameter this. Every reference to class/interface A outside the class is a type reference toA<this: A>. assignment compatibility flows normally like other generic type parameters.
具體的,this 類型在實現上相當於 A<this extends A<A>>(即經典的 CRTP 奇異遞歸模板模式),類中 this 值的類型就是泛型參數 this。出了當前類/接口的上下文,this 的類型就是 A<this: A>,類型兼容性等與泛型一致
所以,this 類型就像一個帶有類派生關係 [約束](/articles/泛型-typescript 筆記 6/#articleHeader8) 的隱式 [類型參數](/articles/泛型-typescript 筆記 6/#articleHeader3)
三。Function this type
除了類/接口外,this 類型還適用於普通函數
不同於 class this type 通常隱式發揮作用(如自動 [類型推斷](/articles/深入類型系統-typescript 筆記 8/#articleHeader1)),function this type 大都通過顯式聲明來約束函數體中 this 值的類型:
This-types for functions allows Typescript authors to specify the type of this that is bound within the function body.
實現原理
把 this 顯式地作為函數的(第一個)參數,從而限定其類型,像普通參數一樣進行類型檢查。例如:
declare class C { m(this: this); }
let c = new C();
// f 類型為 (this:C) => any
let f = c.m;
// 錯誤 The 'this' context of type 'void' is not assignable to method's 'this' of type 'C'.
f();
注意,僅在顯式聲明了 this 值類型時才進行檢查(如上例):
// 去掉顯式聲明的 this 類型
declare class C { m(); }
let c = new C();
// f 類型為 () => any
let f = c.m;
// 正確
f();
P.S. 特殊的,箭頭函數(lambda)的 this 無法手動限定其類型:
let obj = {
x: 1,
// 錯誤 An arrow function cannot have a 'this' parameter.
f: (this: { x: number }) => this.x
};
與 class this type 的關聯
成員方法同時也是函數,兩種 this 類型在這裡產生了交集:
If this is not provided, this is the class' this type for methods.
也就是說,成員方法中,如果沒提供 function this type,那麼就沿用該類/接口的 class this type,類似於自動推斷而來的類型與顯式聲明類型之間的關係:後者能夠覆蓋前者
注意,雖然最初的設計是這樣的(開啟 strictThis/strictThisChecks 選項),但由於性能等方面的原因,後來去掉了該選項。因此,目前 function this type 與 class this type 隱式檢查都很弱(比如未顯式指定 this 類型的成員方法並不預設具有 class this type 約束)
class C {
x = { y: 1 };
f() { return this.x; }
}
let f = new C().f;
// 正確
f();
其中 f 的類型是 () => { y: number; },而不是預期的 (this: C) => { y: number; }
四。應用場景
流式接口(Fluent interface)
this 類型讓 流式接口(fluent interface) 變得很容易描述,例如:
class A {
foo(): this { return this }
}
class B extends A {
bar(): this { return this }
}
new B().foo().bar()
P.S. 所謂的流式接口(設計層面),可以簡單理解為鏈式調用(實現層面):
A fluent interface is a method for designing object oriented APIs based extensively on method chaining with the goal of making the readability of the source code close to that of ordinary written prose, essentially creating a domain-specific language within the interface.
(摘自 Fluent interface)
簡言之,流式接口是 OOP 中的一種 API 設計方式,通過鏈式方法調用讓源碼極具可讀性
描述 this 的類型
function this type 允許我們像描述普通參數一樣限定 this 的類型,這在 Callback 場景尤為重要:
class Cat {
constructor(public name: string) {}
meow(this: Cat) { console.log('meow~'); }
}
class EventBus {
on(type: string, handler: (this: void, ...params) => void) {/* ... */}
}
// 錯誤 Argument of type '(this: Cat) => void' is not assignable to parameter of type '(this: void, ...params: any[]) => void'.
new EventBus().on('click', new Cat('Neko').meow);
(摘自 [this 的類型](/articles/函數-typescript 筆記 5/#articleHeader8))
追蹤 context 類型
有了 this 類型,bind、call、apply 等場景也能正確維持類型約束,要求當前函數 this 與傳入的目標對象類型一致:
apply<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, args: A): R;
call<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, ...args: A): R;
bind<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T): (...args: A) => R;
讓類似的錯誤暴露出來(需要開啟 strictBindCallApply 選項):
class C {
constructor(a: number, b: string) {}
foo(this: this, a: number, b: string): string { return "" }
}
declare let c: C;
let f14 = c.foo.bind(undefined); // Error
let c14 = c.foo.call(undefined, 10, "hello"); // Error
let a14 = c.foo.apply(undefined, [10, "hello"]); // Error
P.S. 關於 bind、call、apply 等類型約束的更多信息,見 Strict bind, call, and apply methods on functions
暫無評論,快來發表你的看法吧