一.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 との関連
メンバーメソッドも同時に関数であり、2 つの 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 を参照
コメントはまだありません