クラスの型にまつわるあれこれ(3)〜クラスにおける特殊なthisと抽象クラスとstatic〜:TypeScriptのTypeあれこれシリーズ(7)
altJS、すなわちJavaScriptの代わりとなる言語の筆頭である「TypeScript」。TypeScriptという言語名が示す通り、JavaScriptに「Type」、つまり型の概念を持ち込んだものです。本連載では、このTypeScriptの型に関して、さまざまな方向から紹介していきます。前回は、クラスの型に関するあれこれを紹介する第2回として、クラスの継承や、クラスをnewしたオブジェクトそのものの型について紹介しました。今回は、クラスの型に関するあれこれを紹介する最後として、クラス内での特殊なthisの使い方、抽象クラス、staticを紹介します。
クラス内での特殊なthisの使い方
前回、前々回で紹介したクラス構文内でのthisの使い方は、同じクラス内や親クラス内のフィールドやメソッドを利用する場合の「this.」という記述でした。今回は、このthisのクラス内での特殊な使い方から話を始めていきます。
thisの特殊な使い方まとめ
クラス内でのthisの特殊な使い方は、以下の3種です。
- メソッドの引数としてのthis
- 戻り値としてのthis
- メソッドの引数の型としてのthis
それぞれ順に見ていきます。
JavaScriptのthisの弊害
最初に紹介するのが、メソッドの引数としてのthisです。ただし、この使い方を紹介する前に、なぜこのような使い方が必要なのか、その原因となるJavaScriptのthisの弊害について簡単に紹介します。JavaScriptのthisは使われる文脈に応じて、その内容がさまざまに変化してしまいます。TypeScriptでも、この性質は引き継がれざるを得ません。
例えば、リスト1のNameHolderクラスがあるとします。
class NameHolder { private name: string = "名無し"; // (1) constructor(name: string) { this.name = name; } showMyName(): void { console.log(`お名前: ${this.name}`); // (2) } showYourName(yourName: string): void { console.log(`私の名前は${this.name}で、あなたのお名前は${yourName}`); // (3) } } const nameHolder = new NameHolder("田中太郎"); // (4) nameHolder.showMyName(); // (5) nameHolder.showYourName("鈴木二郎"); // (6)
このコードの実行結果は、リスト2の通りです。
お名前: 田中太郎 私の名前は田中太郎で、あなたのお名前は鈴木二郎
リスト1の(4)でNameHolderをnewする際に、「田中太郎」を引数で渡しており、これが、(1)のフィールドnameに格納されます。(2)も(3)もこのフィールドnameを表示させています。その際のコードであるthis.nameのthisはNameHolderオブジェクト自身を指し、従って「.name」のnameは(1)のフィールドを指しています。実行結果も、この通りの処理となり問題なく「田中太郎」と表示されています(図1)。
ところが、このNameHolderをnewしたnameHolderを利用して、リスト3のコードを記述したとします。
const func1 = nameHolder.showMyName; // (1) func1(); // (2) const func2 = nameHolder.showYourName; // (3) func2("鈴木二郎"); // (4)
このコードの実行結果は、リスト4の通りです。
お名前: undefined 私の名前はundefinedで、あなたのお名前は鈴木二郎
なぜ、undefinedとなるのでしょうか。この種明かしは図2の通りです。
第3回の関数式でも紹介したように、JavaScriptやTypeScriptでは関数そのものを変数に代入できます。これは、メソッドも同じです。この仕組みを利用してNameHolderのshowMyName()メソッドを変数func1に代入しているのがリスト3の(1)であり、同じく(3)ではfunc2にshowYourName()を代入しています。そしてfunc1とfunc2のそれぞれを、関数として実行しているのが(2)と(4)です。このメソッドを変数に代入した時点で、図2にあるように、func1とfunc2内のthisは、nameHolderではなくグローバルオブジェクトを指すようになり、this.nameはグローバルオブジェクト内の変数nameを指すことになります。これは存在しないので、undefinedとなります。
もう少し続けます。これと同じようなコードとして、例えばリスト5のコードを記述したとします。
const myNameObj = { name: "山本三郎", // (1) func1: nameHolder.showMyName, // (2) func2: nameHolder.showYourName // (3) } myNameObj.func1(); myNameObj.func2("鈴木二郎");
この場合の実行結果は、リスト6の通りです。
お名前: 山本三郎 私の名前は山本三郎で、あなたのお名前は鈴木二郎
先の仕組みが分かると、この実行結果のカラクリも分かると思います。これは図3の通りです。
リスト5の(2)でnameHolderのshowMyName()メソッドをfunc1プロパティとしています。func2プロパティも同様に、nameHolderのshowYourName()メソッドです。この時点で、func1とfunc2内のthisは、nameHolderではなくmyNameObjを指すようになり、this.nameは(1)のnameプロパティを指すことになります。よって実行結果のように、「山本三郎」が表示されてしまいます。
メソッドの引数としてのthis
リスト3やリスト5のような使い方を意図して行うならばいいのですが、通常は意図しないことの方が多いでしょう。そしてそれはバグの原因となります。これを未然に防ぐのが、特殊なthisの使い方の1つ目である、メソッドの引数としてのthisです。これは、リスト7のようなコードです。
class NameHolder { private name: string = "名無し"; constructor(name: string) { this.name = name; } showMyName(this: NameHolder): void { // (1) console.log(`お名前: ${this.name}`); } showYourName(this: NameHolder, yourName: string): void { // (2) console.log(`私の名前は${this.name}で、あなたのお名前は${yourName}`); } }
注目すべきは、(1)と(2)の第1引数の「this: 自クラス名」という記述です。引数そのものにthisを記述し、そのデータ型として自分のクラスそのものを記述します。リスト7では、NameHolderです。この引数は、リスト3やリスト5のような使われ方をしないためのチェックにしか利用されませんので、コンパイル時には削除されます。そのため、リスト1のように通常通りNameHolderをnewして、そのメソッドとしてshowMyName()を利用する場合は、引数なしとして呼び出します。同様に、showYourName()を利用する場合は、第2引数に該当する引数を1つだけ渡します。
一方、リスト3のようなコードを記述した途端、図4のエラーとなります。
同様にリスト5も図5のエラーとなり、安全なコードが実現できます。
戻り値としてのthis
特殊なthisの使い方の2つ目は、thisを戻り値として利用する方法です。これは、例えばNameHolderクラスのshowMyName()メソッドやshowYourName()メソッドを、リスト8のようにしたコードです。なお、フィールドとコンストラクタは省略しています。
class NameHolder { 〜省略〜 showMyName(): this { // (1) console.log(`お名前: ${this.name}`); return this; // (2) } showYourName(yourName: string): this { // (3) console.log(`私の名前は${this.name}で、あなたのお名前は${yourName}`); return this; // (4) } }
リスト1やリスト7のメソッドは、戻り値なし(void型)でした。一方リスト8では、(1)や(3)にあるようにthisが戻り値の型となっています。このthisは自分自身を指し、この戻り値の型に合わせるように、(2)や(4)のようにthisをリターンしています。
この自分自身を戻り値とするメソッドの利点は、リスト9のようにメソッドチェーンが可能となることです。なお、メソッドチェーンとは、1つの文で同一オブジェクトのメソッドを連続して呼び出す方法です。
const nameHolder = new NameHolder("田中太郎"); nameHolder.showMyName().showYourName("鈴木二郎");
引数のthisと戻り値のthisの併用
このthisを戻り値の型とする記述と、前項で紹介した引数のthisは併用できません。これはすなわち、リスト10のようなコードです。
showMyName(this: NameHolder): this { console.log(`お名前: ${this.name}`); return this; }
この場合、引数として渡されたthisをリターンするコードと判断されるため、リターンするthisの型であるNameHolderと、戻り値の型記述であるthisとの間に型の不一致が発生するからです。実際、図6のエラーとなります。
もし、引数のthisと戻り値のthisを併用したい場合は、戻り値の型としてthisを記述しないでおきます。これはすなわち、リスト11のコードです。
showMyName(this: NameHolder) { console.log(`お名前: ${this.name}`); return this; }
この場合はエラーがなくなり、コンパイルが通ります。しかも、問題なく自分自身のthisがリターンされる動作になります。この種明かしは、前項で解説しています。引数のthisはコンパイル時に削除されるため、戻り値のthisは、コンパイル時には引数のthisではなく自分自身を指すthisになるからです。
メソッドの引数の型としてのthis
特殊なthisの使い方として最後に紹介するのは、引数の型としてthisを記述する方法です。例えば、リスト12のようなコードです。
class NameHolder { name: string = "名無し"; constructor(name: string) { this.name = name; } showDoubleName(holder: this): void { // (1) console.log(`フィールドのお名前は${this.name}で、引数のお名前は${holder.name}`); } showDoubleName2(holder: NameHolder): void { // (2) console.log(`フィールドのお名前は${this.name}で、引数のお名前は${holder.name}`); } }
リスト12の(1)のメソッドの引数の型に注目してください。特殊なthisの使い方の1つ目として紹介したのは、引数そのものにthisを記述する方法でした。こちらは、それとは違い型にthisを記述しています。
このthisも自分自身を指します。となると(2)のように、自分自身のクラス名を型として記述するのと同じではないか、と思うかもしれません。この違いが出てくるのは、子クラスを作成した場合です。例えば、子クラスとしてリスト13のようなExtendedNameHolderがあるとします。
class ExtendedNameHolder extends NameHolder { showName():void { console.log(`フィールドのお名前は${this.name}`); } }
これらのクラスを利用するコードとして、リスト14のコードを記述したとします。
const nameHolderTaro = new NameHolder("田中太郎"); // (1) const nameHolderJiro = new NameHolder("鈴木二郎"); // (2) nameHolderJiro.showDoubleName(nameHolderTaro); // (3) nameHolderJiro.showDoubleName2(nameHolderTaro); // (4) const nameHolderSaburo = new ExtendedNameHolder("山本三郎"); // (5) nameHolderSaburo.showDoubleName(nameHolderTaro); // (6) nameHolderSaburo.showDoubleName2(nameHolderTaro); // (7)
(1)と(2)でNameHolderを、それぞれ「田中太郎」と「鈴木二郎」でnewしています。この「鈴木二郎」を表すnameHolderJiroに対して、「田中太郎」のnameHolderTaroを引数として、(3)でshowDoubleName()メソッドを、(4)でshowDoubleName2()メソッドを実行しています。NameHolderクラスのshowDoubleName()メソッドもshowDoubleName2()メソッドも、引数の型は自分自身を表すので、ここまでは問題なく動きます。
同じように、(5)で子クラスであるExtendedNameHolderを「山本三郎」でnewし、このオブジェクトであるnameHolderSaburoに対して、「田中太郎」のnameHolderTaroを引数として、(6)でshowDoubleName()メソッドを、(7)でshowDoubleName2()メソッドを実行しようとしています。しかし、この(6)で図7のエラーとなります。一方、(7)はエラーとなりません。
showDoubleName()の引数の型であるthisは、自分自身を指すため、NameHolder中では、NameHolderを指します。一方、子クラスであるExtendedNameHolder中ではExtendedNameHolderを指します。しかし、(6)ではExtendedNameHolderではなくNameHolder型であるnameHolderTaroを渡しています。この型の不一致が、エラーの原因です。
一方、(7)のshowDoubleName2()メソッドの引数の型は、NameHolder型であり、nameHolderTaroの型と一致します。これは、当然エラーとなりません。
抽象クラス
thisの話はこの辺りで終え、次の抽象クラスに話を移します。
抽象クラスとは
まず、サンプルを見ていただきます。
abstract class NameHolder { // (1) protected name: string = "名無し"; constructor(name: string) { this.name = name; } abstract showName(): void; // (2) }
注目するのは、リスト15の(2)です。このメソッドshowName()には処理ブロックがなく、メソッドシグネチャだけとなっています。このようなメソッドのことを「抽象メソッド」といい、メソッドには必ず「abstract」を記述することになっています。このabstractを記述し忘れると、図8のエラーとなります。
そして、抽象メソッドを含むクラスは、リスト15の(1)のように、そのクラス宣言にもabstractを記述することになっています。こちらも、記述し忘れるとエラーとなるので注意してください。
抽象クラスの使い方
この抽象クラスの使い方には注意が必要です。まず、抽象クラス本体はnewできません。newしようとすると図9のエラーとなります。
この抽象クラスは、継承のために存在します。すなわち子クラスを作成して、そのクラスを利用します。例えば、リスト16のコードです。
class NameHolderJa extends NameHolder { showName(): void { console.log(`お名前: ${this.name}`); } } const nameHolderTaro = new NameHolderJa("田中太郎"); nameHolderTaro.showName();
この子クラスを作成する際、抽象メソッドを必ずオーバーライド(実装)する必要があります。もし、実装し忘れると、図10のエラーとなります。
このことから、子クラスに必ずオーバーライドしてほしいメソッドがある場合は、あえてそのメソッドを抽象メソッドとする抽象クラスを作成することにより、オーバーライド忘れを防ぐことができます。
static
最後にstaticについて軽く紹介しておきましょう。といっても、もともとJavaScriptのクラス構文にstaticはあります。そしてTypeScriptでも、JavaScriptのstaticとなんら変わりはありません。例えば、リスト17のようなコードです。
class Calc { static withTax(val: number): number { // (1) return val * 1.1; } } const ans = Calc.withTax(1244); // (2)
(1)のようにstaticキーワードを使って宣言されたメソッドが、staticメソッドです。JavaScriptとの違いは、引数や戻り値の型記述があることぐらいで、使い方はJavaScriptと同じです。(2)のようにクラスをnewせずに利用できます。
このstaticメソッドからフィールドを利用する場合、フィールドもstaticにする必要があります。例えば、リスト18のようなコードです。
class PointManager { private static point = 0; static incrementPoint(): number { return ++this.point; } }
なお、リスト18にあるように、staticメンバにもアクセス修飾子は利用できます。
まとめ
TypeScriptのTypeに注目し、あれこれ紹介する本連載の第7回はいかがでしたか?
クラスの型に関して、3回にわたってあれこれ紹介してきました。その最後である今回は、クラス内での特殊なthisの使い方、抽象クラス、staticを紹介しました。クラスに関しては、これで一通り終了です。次回は、データ型を後から指定できるジェネリクスについて紹介します。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.