メソッド戻り値のthis型とメソッドチェーン
this型とは
TypeScriptではメソッドの戻り値の型としてthisを指定できます。this型は「このクラス自身の型」を表す特殊な型です。
tsclassParent {returnParent ():Parent {return this;}returnThis (): this {return this;}}constp = newParent ();consta =p .returnParent ();constb =p .returnThis ();
tsclassParent {returnParent ():Parent {return this;}returnThis (): this {return this;}}constp = newParent ();consta =p .returnParent ();constb =p .returnThis ();
Parentインスタンスから呼び出すと、どちらも戻り値の型はParentです。一見すると、クラス名を直接書くのと同じように思えます。
しかし、継承関係において重要な違いがあります。Parentを継承したChildクラスを作り、Childのインスタンスからメソッドを呼び出してみましょう。
tsclassChild extendsParent {}constchild = newChild ();consta =child .returnParent ();constb =child .returnThis ();
tsclassChild extendsParent {}constchild = newChild ();consta =child .returnParent ();constb =child .returnThis ();
returnParentは戻り値の型をParentと明示しています。そのため、Childインスタンスから呼び出しても、戻り値の型はParentのままです。
一方、returnThisは戻り値の型をthisとしています。Childインスタンスから呼び出すと、戻り値の型はChildになります。
このように、this型は呼び出し元のクラスに応じて変化する型です。
this型が必要になるとき
この違いが問題になるのは、サブクラスで親クラスのメソッドを呼び出した後、サブクラス固有のメソッドを続けて呼びたいときです。
tsclassParent {foo ():Parent {return this;}}classChild extendsParent {bar ():Child {return this;}}constchild = newChild ();child .bar ().foo (); // OK: Child -> ParentProperty 'bar' does not exist on type 'Parent'.2339Property 'bar' does not exist on type 'Parent'.child .foo ().(); // Error! bar
tsclassParent {foo ():Parent {return this;}}classChild extendsParent {bar ():Child {return this;}}constchild = newChild ();child .bar ().foo (); // OK: Child -> ParentProperty 'bar' does not exist on type 'Parent'.2339Property 'bar' does not exist on type 'Parent'.child .foo ().(); // Error! bar
child.foo()の戻り値はParent型なので、Childにしかないbarメソッドが呼び出せません。
戻り値の型をthisに変更すると、この問題が解消されます。
tsclassParent {foo (): this {return this;}}classChild extendsParent {bar (): this {return this;}}constchild = newChild ();child .bar ().foo (); // OKchild .foo ().bar (); // OK
tsclassParent {foo (): this {return this;}}classChild extendsParent {bar (): this {return this;}}constchild = newChild ();child .bar ().foo (); // OKchild .foo ().bar (); // OK
child.foo()の戻り値がChild型になるため、続けてbarを呼び出せます。
なお、this型は型チェックを回避しているわけではありません。むしろ、継承関係においてより正確な型情報を提供しています。戻り値をクラス名で固定すると、サブクラスで呼び出しても常に親クラスの型として扱われてしまいます。this型を使うことで、実際のクラスに応じた適切な型が推論されます。
応用例: fluent interface
this型の実用的な応用例として、fluent interfaceがあります。fluent interfaceとは「流れるようなインターフェース」という意味で、メソッドチェーンを使って可読性の高いコードを実現するメソッドの作り方のことです。よくドメイン固有言語(DSL)を提供するようなクラスを作るときに使われます。
商品検索のクエリを組み立てるクラスを考えます。
tsclassProductQuery {protectedparams = newURLSearchParams ();category (name : string): void {this.params .set ("category",name );}maxPrice (price : number): void {this.params .set ("maxPrice",String (price ));}minReview (rating : number): void {this.params .set ("minReview",String (rating ));}toString (): string {return this.params .toString ();}}constquery = newProductQuery ();query .category ("Pizza");query .maxPrice (2000);query .minReview (4);console .log (query .toString ());
tsclassProductQuery {protectedparams = newURLSearchParams ();category (name : string): void {this.params .set ("category",name );}maxPrice (price : number): void {this.params .set ("maxPrice",String (price ));}minReview (rating : number): void {this.params .set ("minReview",String (rating ));}toString (): string {return this.params .toString ();}}constquery = newProductQuery ();query .category ("Pizza");query .maxPrice (2000);query .minReview (4);console .log (query .toString ());
メソッドごとにステートメントを分ける必要があります。メソッドチェーンを使って処理を連続させるには、各メソッドがthisを返すようにします。
tsclassProductQuery {protectedparams = newURLSearchParams ();category (name : string): this {this.params .set ("category",name );return this;}maxPrice (price : number): this {this.params .set ("maxPrice",String (price ));return this;}minReview (rating : number): this {this.params .set ("minReview",String (rating ));return this;}toString (): string {return this.params .toString ();}}constquery = newProductQuery ().category ("Pizza").maxPrice (2000).minReview (4).toString ();console .log (query );
tsclassProductQuery {protectedparams = newURLSearchParams ();category (name : string): this {this.params .set ("category",name );return this;}maxPrice (price : number): this {this.params .set ("maxPrice",String (price ));return this;}minReview (rating : number): this {this.params .set ("minReview",String (rating ));return this;}toString (): string {return this.params .toString ();}}constquery = newProductQuery ().category ("Pizza").maxPrice (2000).minReview (4).toString ();console .log (query );
戻り値の型をthisにすることで、メソッドチェーンが可能になりました。
ここで、このProductQueryを拡張して、スマートフォン検索専用のクエリクラスを作りたいとします。this型を使っているおかげで、サブクラスでもメソッドチェーンが正しく動作します。
tsclassProductQuery {protectedparams = newURLSearchParams ();category (name : string): this {this.params .set ("category",name );return this;}maxPrice (price : number): this {this.params .set ("maxPrice",String (price ));return this;}minReview (rating : number): this {this.params .set ("minReview",String (rating ));return this;}toString (): string {return this.params .toString ();}}classSmartphoneQuery extendsProductQuery {os (name : string): this {this.params .set ("os",name );return this;}maker (name : string): this {this.params .set ("maker",name );return this;}}constquery = newSmartphoneQuery ().maxPrice (100000).minReview (4).os ("Android").maker ("Google").toString ();console .log (query );
tsclassProductQuery {protectedparams = newURLSearchParams ();category (name : string): this {this.params .set ("category",name );return this;}maxPrice (price : number): this {this.params .set ("maxPrice",String (price ));return this;}minReview (rating : number): this {this.params .set ("minReview",String (rating ));return this;}toString (): string {return this.params .toString ();}}classSmartphoneQuery extendsProductQuery {os (name : string): this {this.params .set ("os",name );return this;}maker (name : string): this {this.params .set ("maker",name );return this;}}constquery = newSmartphoneQuery ().maxPrice (100000).minReview (4).os ("Android").maker ("Google").toString ();console .log (query );
もし戻り値の型をProductQueryと明示していた場合、maxPriceメソッドの戻り値はProductQuery型になり、SmartphoneQueryにしかないosやmakerメソッドが呼び出せなくなります。this型を使うことで、継承階層でもメソッドチェーンを安全に実現できます。