背景資料
クラス (MDN)
TypeScriptは、ES2015で導入されたclass
キーワードを完全にサポートしています。
他のJavaScript言語機能と同様に、TypeScriptは型注釈やその他の構文を追加して、クラスと他の型との関係を表現できるようにします。
クラスメンバー
これが最も基本的なクラス、つまり空のクラスです。
tsTry
classPoint {}
このクラスはまだあまり役に立たないので、メンバーを追加することから始めましょう。
フィールド
フィールド宣言は、クラスにpublicな書き込み可能なプロパティを作成します。
tsTry
classPoint {x : number;y : number;}constpt = newPoint ();pt .x = 0;pt .y = 0;
他の場所と同様に、型注釈はオプションですが、指定しない場合は暗黙的にany
になります。
フィールドには初期化子を持たせることもできます。これらはクラスがインスタンス化されるときに自動的に実行されます。
tsTry
classPoint {x = 0;y = 0;}constpt = newPoint ();// Prints 0, 0console .log (`${pt .x }, ${pt .y }`);
const
、let
、およびvar
と同様に、クラスプロパティの初期化子は、その型を推論するために使用されます。
tsTry
constpt = newPoint ();Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.pt .x = "0";
--strictPropertyInitialization
strictPropertyInitialization
設定は、クラスフィールドをコンストラクターで初期化する必要があるかどうかを制御します。
tsTry
classBadGreeter {Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.: string; name }
tsTry
classGoodGreeter {name : string;constructor() {this.name = "hello";}}
フィールドはコンストラクター自体で初期化する必要があることに注意してください。 TypeScriptは、コンストラクターから呼び出すメソッドを分析して初期化を検出することはありません。これは、派生クラスがこれらのメソッドをオーバーライドし、メンバーを初期化できない可能性があるためです。
コンストラクター以外の手段(たとえば、外部ライブラリがクラスの一部を埋めている場合など)でフィールドを確実に初期化する場合は、definite assignment assertion operatorである!
を使用できます。
tsTry
classOKGreeter {// Not initialized, but no errorname !: string;}
readonly
フィールドには、readonly
修飾子を付けることができます。これにより、コンストラクター外でのフィールドへの代入を防ぎます。
tsTry
classGreeter {readonlyname : string = "world";constructor(otherName ?: string) {if (otherName !==undefined ) {this.name =otherName ;}}err () {this.Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.= "not ok"; name }}constg = newGreeter ();Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.g .= "also not ok"; name
コンストラクター
背景資料
コンストラクター(MDN)
クラスコンストラクターは、関数と非常によく似ています。 型注釈、デフォルト値、オーバーロードを使用してパラメーターを追加できます。
tsTry
classPoint {x : number;y : number;// Normal signature with defaultsconstructor(x = 0,y = 0) {this.x =x ;this.y =y ;}}
tsTry
classPoint {// Overloadsconstructor(x : number,y : string);constructor(s : string);constructor(xs : any,y ?: any) {// TBD}}
クラスコンストラクターのシグネチャと関数のシグネチャには、いくつかの違いがあります。
- コンストラクターは型パラメーターを持つことはできません。これらは外側のクラス宣言に属します。これについては後で学習します。
- コンストラクターは戻り値の型注釈を持つことはできません。クラスインスタンス型は常に返されるものです。
スーパーコール
JavaScriptと同様に、基底クラスがある場合、コンストラクタの本体でthis.
メンバを使用する前にsuper();
を呼び出す必要があります。
tsTry
classBase {k = 4;}classDerived extendsBase {constructor() {// Prints a wrong value in ES5; throws exception in ES6'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.console .log (this .k );super();}}
super
の呼び出し忘れはJavaScriptで起こりがちなミスですが、TypeScriptは必要な場合にそれを教えてくれます。
メソッド
背景資料
メソッド定義
クラスの関数プロパティはメソッドと呼ばれます。メソッドは、関数やコンストラクタと同じ型注釈をすべて使用できます。
tsTry
classPoint {x = 10;y = 10;scale (n : number): void {this.x *=n ;this.y *=n ;}}
標準の型注釈を除けば、TypeScriptはメソッドに新しいものを何も追加しません。
メソッド本体内では、フィールドや他のメソッドにthis.
経由でアクセスする必要があることに注意してください。メソッド本体内の修飾されていない名前は、常に外側のスコープの何かを参照します。
tsTry
letx : number = 0;classC {x : string = "hello";m () {// This is trying to modify 'x' from line 1, not the class propertyType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.= "world"; x }}
ゲッター/セッター
クラスはアクセサーを持つこともできます。
tsTry
classC {_length = 0;getlength () {return this._length ;}setlength (value ) {this._length =value ;}}
追加のロジックがない、フィールドにバックアップされたget/setのペアは、JavaScriptでは非常にまれにしか役に立ちません。get/set操作中に追加のロジックを追加する必要がない場合は、publicフィールドを公開しても構いません。
TypeScriptには、アクセサーに関する特別な推論ルールがいくつかあります。
get
が存在するがset
が存在しない場合、プロパティは自動的にreadonly
になります。- セッターパラメータの型が指定されていない場合、ゲッターの戻り型から推論されます。
- ゲッターとセッターは、同じメンバーの可視性を持つ必要があります。
TypeScript 4.3以降、取得と設定で異なる型を持つアクセサーを持つことができます。
tsTry
classThing {_size = 0;getsize (): number {return this._size ;}setsize (value : string | number | boolean) {letnum =Number (value );// Don't allow NaN, Infinity, etcif (!Number .isFinite (num )) {this._size = 0;return;}this._size =num ;}}
インデックスシグネチャ
クラスはインデックスシグネチャを宣言できます。これらは他のオブジェクト型のインデックスシグネチャと同じように機能します。
tsTry
classMyClass {[s : string]: boolean | ((s : string) => boolean);check (s : string) {return this[s ] as boolean;}}
インデックスシグネチャ型はメソッドの型もキャプチャする必要があるため、これらの型を効果的に使用するのは簡単ではありません。一般に、インデックス付きデータをクラスインスタンス自体ではなく、別の場所に保存する方が優れています。
クラスの継承
オブジェクト指向の機能を持つ他の言語と同様に、JavaScriptのクラスは基底クラスから継承できます。
implements
句
implements
句を使用して、クラスが特定のinterface
を満たしているかどうかを確認できます。クラスが正しく実装できなかった場合、エラーが発行されます。
tsTry
interfacePingable {ping (): void;}classSonar implementsPingable {ping () {console .log ("ping!");}}classClass 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.2420Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.implements Ball Pingable {pong () {console .log ("pong!");}}
クラスは複数のインターフェースを実装することもできます。例:class C implements A, B {
。
注意事項
implements
句は、クラスがインターフェース型として扱えるかどうかを確認するだけであるということを理解することが重要です。クラスまたはそのメソッドの型をまったく変更しません。よくあるエラーの原因は、implements
句がクラスの型を変更すると想定することです。そうではありません!
tsTry
interfaceCheckable {check (name : string): boolean;}classNameChecker implementsCheckable {Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.check () { s // Notice no error herereturns .toLowerCase () === "ok";}}
この例では、おそらくs
の型がcheck
のname: string
パラメータの影響を受けることを期待していました。そうではありません。implements
句は、クラス本体がどのようにチェックされるか、またはその型がどのように推論されるかを変更しません。
同様に、オプションのプロパティを持つインターフェースを実装しても、そのプロパティは作成されません。
tsTry
interfaceA {x : number;y ?: number;}classC implementsA {x = 0;}constc = newC ();Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.c .= 10; y
extends
句
背景資料
extendsキーワード(MDN)
クラスは基底クラスからextend
できます。派生クラスは、基底クラスのすべてのプロパティとメソッドを持ち、追加のメンバーを定義することもできます。
tsTry
classAnimal {move () {console .log ("Moving along!");}}classDog extendsAnimal {woof (times : number) {for (leti = 0;i <times ;i ++) {console .log ("woof!");}}}constd = newDog ();// Base class methodd .move ();// Derived class methodd .woof (3);
メソッドのオーバーライド
背景資料
superキーワード(MDN)
派生クラスは、基底クラスのフィールドまたはプロパティをオーバーライドすることもできます。基底クラスのメソッドにアクセスするには、super.
構文を使用できます。JavaScriptのクラスは単純なルックアップオブジェクトであるため、「スーパーフィールド」の概念はないことに注意してください。
TypeScriptは、派生クラスが常に基底クラスのサブタイプであることを強制します。
たとえば、メソッドをオーバーライドする合法的な方法を次に示します。
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {greet (name ?: string) {if (name ===undefined ) {super.greet ();} else {console .log (`Hello, ${name .toUpperCase ()}`);}}}constd = newDerived ();d .greet ();d .greet ("reader");
派生クラスが基底クラスの契約に従うことが重要です。派生クラスのインスタンスを基底クラスの参照を通じて参照するのは非常に一般的であり、常に合法であることを忘れないでください。
tsTry
// Alias the derived instance through a base class referenceconstb :Base =d ;// No problemb .greet ();
Derived
がBase
の契約に従わなかった場合はどうなるでしょうか?
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {// Make this parameter requiredProperty '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'.2416Property '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'.( greet name : string) {console .log (`Hello, ${name .toUpperCase ()}`);}}
エラーにもかかわらずこのコードをコンパイルした場合、このサンプルはクラッシュします。
tsTry
constb :Base = newDerived ();// Crashes because "name" will be undefinedb .greet ();
型のみのフィールド宣言
target >= ES2022
またはuseDefineForClassFields
がtrue
の場合、クラスフィールドは親クラスのコンストラクタが完了した後に初期化され、親クラスによって設定された値を上書きします。これは、継承されたフィールドに対してより正確な型を再宣言したいだけの場合に問題となる可能性があります。これらのケースを処理するために、TypeScriptにこのフィールド宣言にランタイム効果がないことを示すためにdeclare
を記述できます。
tsTry
interfaceAnimal {dateOfBirth : any;}interfaceDog extendsAnimal {breed : any;}classAnimalHouse {resident :Animal ;constructor(animal :Animal ) {this.resident =animal ;}}classDogHouse extendsAnimalHouse {// Does not emit JavaScript code,// only ensures the types are correctdeclareresident :Dog ;constructor(dog :Dog ) {super(dog );}}
初期化順序
JavaScriptクラスが初期化される順序は、場合によっては驚くべきものです。このコードを考えてみましょう。
tsTry
classBase {name = "base";constructor() {console .log ("My name is " + this.name );}}classDerived extendsBase {name = "derived";}// Prints "base", not "derived"constd = newDerived ();
ここで何が起こったのでしょうか?
JavaScriptで定義されているクラスの初期化順序は次のとおりです。
- 基底クラスのフィールドが初期化されます。
- 基底クラスのコンストラクタが実行されます。
- 派生クラスのフィールドが初期化されます。
- 派生クラスのコンストラクタが実行されます。
つまり、派生クラスのフィールド初期化がまだ実行されていないため、基底クラスのコンストラクタは自身のコンストラクタ中にname
の自身の値を確認しました。
組み込み型の継承
注:
Array
、Error
、Map
などの組み込み型から継承する予定がない場合、またはコンパイルターゲットが明示的にES6
/ES2015
以上に設定されている場合は、このセクションをスキップできます。
ES2015では、オブジェクトを返すコンストラクタは、super(...)
のすべての呼び出し元に対してthis
の値を暗黙的に置き換えます。生成されたコンストラクタコードがsuper(...)
の潜在的な戻り値をキャプチャし、それをthis
に置き換える必要があります。
結果として、Error
、Array
などをサブクラス化すると、期待どおりに機能しなくなる場合があります。これは、Error
、Array
などのコンストラクタ関数がECMAScript 6のnew.target
を使用してプロトタイプチェーンを調整するためです。ただし、ECMAScript 5でコンストラクタを呼び出すときにnew.target
の値を確認する方法はありません。他のダウンレベルコンパイラも、デフォルトでは通常同じ制限があります。
次のようなサブクラスの場合
tsTry
classMsgError extendsError {constructor(m : string) {super(m );}sayHello () {return "hello " + this.message ;}}
次のようなことがわかります。
- これらのサブクラスを構築して返されたオブジェクトでメソッドが
undefined
になる場合があるため、sayHello
を呼び出すとエラーになります。 instanceof
はサブクラスのインスタンスとそのインスタンスの間で壊れるため、(new MsgError()) instanceof MsgError
はfalse
を返します。
推奨事項として、super(...)
呼び出しの直後にプロトタイプを手動で調整できます。
tsTry
classMsgError extendsError {constructor(m : string) {super(m );// Set the prototype explicitly.Object .setPrototypeOf (this,MsgError .prototype );}sayHello () {return "hello " + this.message ;}}
ただし、MsgError
のサブクラスは、プロトタイプも手動で設定する必要があります。 Object.setPrototypeOf
をサポートしないランタイムでは、代わりに __proto__
を使用できる場合があります。
残念ながら、これらの回避策は Internet Explorer 10 以前では機能しません。 プロトタイプからインスタンス自体にメソッドを手動でコピーする(つまり、MsgError.prototype
を this
にコピーする)ことはできますが、プロトタイプチェーン自体を修正することはできません。
メンバーの可視性
TypeScript を使用して、特定のメソッドまたはプロパティをクラス外のコードから可視にするかどうかを制御できます。
public
クラスメンバーのデフォルトの可視性は public
です。 public
メンバーはどこからでもアクセスできます。
tsTry
classGreeter {publicgreet () {console .log ("hi!");}}constg = newGreeter ();g .greet ();
public
はすでにデフォルトの可視性修飾子であるため、クラスメンバーに記述する必要はありませんが、スタイルや読みやすさのために記述することもできます。
protected
protected
メンバーは、宣言されているクラスのサブクラスからのみ可視です。
tsTry
classGreeter {publicgreet () {console .log ("Hello, " + this.getName ());}protectedgetName () {return "hi";}}classSpecialGreeter extendsGreeter {publichowdy () {// OK to access protected member hereconsole .log ("Howdy, " + this.getName ());}}constg = newSpecialGreeter ();g .greet (); // OKProperty 'getName' is protected and only accessible within class 'Greeter' and its subclasses.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.g .(); getName
protected
メンバーの公開
派生クラスは、基底クラスのコントラクトに従う必要がありますが、より多くの機能を持つ基底クラスのサブタイプを公開することを選択できます。 これには、protected
メンバーを public
にすることが含まれます。
tsTry
classBase {protectedm = 10;}classDerived extendsBase {// No modifier, so default is 'public'm = 15;}constd = newDerived ();console .log (d .m ); // OK
Derived
はすでに m
を自由に読み書きできることに注意してください。したがって、これはこの状況の「セキュリティ」を意味のある形で変更するものではありません。 ここで注目すべき主な点は、派生クラスでは、この公開が意図的でない場合は、protected
修飾子を繰り返すように注意する必要があるということです。
階層間の protected
アクセス
さまざまな OOP 言語では、基底クラスの参照を介して protected
メンバーにアクセスすることが合法であるかどうかについて意見が異なります。
tsTry
classBase {protectedx : number = 1;}classDerived1 extendsBase {protectedx : number = 5;}classDerived2 extendsBase {f1 (other :Derived2 ) {other .x = 10;}f2 (other :Derived1 ) {Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.2445Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.other .= 10; x }}
たとえば、Java では、これは合法であると見なされます。 一方、C# と C++ では、このコードは違法であると判断しました。
TypeScript はここで C# および C++ を支持します。なぜなら、Derived2
での x
へのアクセスは、Derived2
のサブクラスからのみ合法である必要があり、Derived1
はそのいずれでもないからです。 さらに、Derived1
の参照を介した x
へのアクセスが違法である場合(確かにそうである必要があります!)、基底クラスの参照を介したアクセスによって状況が改善されることはありません。
C# の推論の詳細については、派生クラスから protected メンバーにアクセスできないのはなぜですか? も参照してください。
private
private
は protected
に似ていますが、サブクラスからのメンバーへのアクセスも許可しません。
tsTry
classBase {privatex = 0;}constb = newBase ();// Can't access from outside the classProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (b .); x
tsTry
classDerived extendsBase {showX () {// Can't access in subclassesProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (this.); x }}
private
メンバーは派生クラスから可視ではないため、派生クラスはそれらの可視性を高めることができません。
tsTry
classBase {privatex = 0;}classClass 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.extends Derived Base {x = 1;}
インスタンス間の private
アクセス
さまざまな OOP 言語では、同じクラスの異なるインスタンスが互いの private
メンバーにアクセスできるかどうかについて意見が異なります。 Java、C#、C++、Swift、PHP などの言語ではこれが許可されていますが、Ruby では許可されていません。
TypeScript では、インスタンス間の private
アクセスが許可されています。
tsTry
classA {privatex = 10;publicsameAs (other :A ) {// No errorreturnother .x === this.x ;}}
注意点
TypeScript の型システムの他の側面と同様に、private
と protected
は 型チェック中にのみ適用されます。
これは、in
や単純なプロパティルックアップなどの JavaScript ランタイムの構造で、private
または protected
メンバーにアクセスできることを意味します。
tsTry
classMySafe {privatesecretKey = 12345;}
js
// In a JavaScript file...const s = new MySafe();// Will print 12345console.log(s.secretKey);
private
は、型チェック中にブラケット表記を使用してアクセスすることもできます。 これにより、private
で宣言されたフィールドは、単体テストなどの目的でアクセスしやすくなる可能性があります。欠点として、これらのフィールドは *ソフトプライベート* であり、プライバシーを厳密に強制するものではありません。
tsTry
classMySafe {privatesecretKey = 12345;}consts = newMySafe ();// Not allowed during type checkingProperty 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.console .log (s .); secretKey // OKconsole .log (s ["secretKey"]);
TypeScript の private
とは異なり、JavaScript の プライベートフィールド(#
)はコンパイル後もプライベートのままであり、ブラケット表記アクセスのような前述のエスケープハッチは提供されないため、*ハードプライベート* になります。
tsTry
classDog {#barkAmount = 0;personality = "happy";constructor() {}}
tsTry
"use strict";class Dog {#barkAmount = 0;personality = "happy";constructor() { }}
ES2021 以前にコンパイルする場合、TypeScript は #
の代わりに WeakMaps を使用します。
tsTry
"use strict";var _Dog_barkAmount;class Dog {constructor() {_Dog_barkAmount.set(this, 0);this.personality = "happy";}}_Dog_barkAmount = new WeakMap();
クラス内の値を悪意のあるアクターから保護する必要がある場合は、クロージャ、WeakMaps、プライベートフィールドなど、ハードランタイムプライバシーを提供するメカニズムを使用する必要があります。 ランタイム中のこれらの追加のプライバシーチェックは、パフォーマンスに影響を与える可能性があることに注意してください。
静的メンバー
背景資料
静的メンバー (MDN)
クラスには static
メンバーを含めることができます。 これらのメンバーは、クラスの特定のインスタンスに関連付けられていません。 クラスコンストラクターオブジェクト自体を介してアクセスできます。
tsTry
classMyClass {staticx = 0;staticprintX () {console .log (MyClass .x );}}console .log (MyClass .x );MyClass .printX ();
静的メンバーは、同じ public
、protected
、および private
の可視性修飾子も使用できます。
tsTry
classMyClass {private staticx = 0;}Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.console .log (MyClass .); x
静的メンバーも継承されます。
tsTry
classBase {staticgetGreeting () {return "Hello world";}}classDerived extendsBase {myGreeting =Derived .getGreeting ();}
特別な静的名
一般に、Function
プロトタイプからプロパティを上書きすることは安全ではなく、不可能です。 クラスは、new
で呼び出すことができる関数自体であるため、特定の static
名は使用できません。 name
、length
、call
などの関数プロパティは、static
メンバーとして定義するには無効です。
tsTry
classS {staticStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.= "S!"; name }
静的クラスがない理由?
TypeScript(および JavaScript)には、例えば C# のようにstatic class
と呼ばれる構造体はありません。
これらの構造体は、これらの言語がすべてのデータと関数をクラス内に強制的に置くようにしているためだけに存在します。TypeScript ではそのような制限がないため、それらは必要ありません。単一のインスタンスしか持たないクラスは、通常、JavaScript/TypeScript では通常のオブジェクトとして表現されます。
例えば、TypeScript では、通常のオブジェクト(またはトップレベル関数)が同じように機能するため、「static class」構文は必要ありません。
tsTry
// Unnecessary "static" classclassMyStaticClass {staticdoSomething () {}}// Preferred (alternative 1)functiondoSomething () {}// Preferred (alternative 2)constMyHelperObject = {dosomething () {},};
クラス内のstatic
ブロック
static ブロックを使用すると、包含クラス内のプライベートフィールドにアクセスできる独自のスコープを持つステートメントのシーケンスを記述できます。これは、ステートメントを記述するすべての機能を備えた初期化コードを記述でき、変数のリークがなく、クラスの内部に完全にアクセスできることを意味します。
tsTry
classFoo {static #count = 0;getcount () {returnFoo .#count;}static {try {constlastInstances =loadLastInstances ();Foo .#count +=lastInstances .length ;}catch {}}}
ジェネリッククラス
クラスは、インターフェースと同様に、ジェネリックにすることができます。ジェネリッククラスがnew
でインスタンス化されると、その型パラメータは関数呼び出しと同じように推論されます。
tsTry
classBox <Type > {contents :Type ;constructor(value :Type ) {this.contents =value ;}}constb = newBox ("hello!");
クラスは、インターフェースと同じようにジェネリック制約とデフォルトを使用できます。
静的メンバーの型パラメータ
このコードは合法ではなく、その理由が明確でない可能性があります。
tsTry
classBox <Type > {staticStatic members cannot reference class type parameters.2302Static members cannot reference class type parameters.defaultValue :; Type }
型は常に完全に消去されることを忘れないでください。ランタイムでは、Box.defaultValue
プロパティのスロットは1つしかありません。これは、Box<string>.defaultValue
を設定すると(それが可能であれば)、Box<number>.defaultValue
も変更されることを意味します。これは良くありません。ジェネリッククラスのstatic
メンバーは、クラスの型パラメータを参照することはできません。
クラス内の実行時におけるthis
背景資料
this キーワード(MDN)
TypeScript は JavaScript のランタイム動作を変更しないこと、そして JavaScript がいくつかの独特なランタイム動作を持つことで有名であることを覚えておくことが重要です。
JavaScript の this
の扱いは、確かに通常とは異なります。
tsTry
classMyClass {name = "MyClass";getName () {return this.name ;}}constc = newMyClass ();constobj = {name : "obj",getName :c .getName ,};// Prints "obj", not "MyClass"console .log (obj .getName ());
手短に言うと、デフォルトでは、関数内の this
の値は、関数がどのように呼び出されたかに依存します。この例では、関数が obj
参照を介して呼び出されたため、this
の値はクラスインスタンスではなく obj
でした。
これはほとんどの場合、望ましい動作ではありません。TypeScript には、この種のエラーを軽減または防止するためのいくつかの方法が用意されています。
アロー関数
背景資料
アロー関数 (MDN)
this
コンテキストを失う方法で頻繁に呼び出される関数がある場合は、メソッド定義の代わりにアロー関数プロパティを使用するのが理にかなっている場合があります。
tsTry
classMyClass {name = "MyClass";getName = () => {return this.name ;};}constc = newMyClass ();constg =c .getName ;// Prints "MyClass" instead of crashingconsole .log (g ());
これにはいくつかのトレードオフがあります。
this
の値は、TypeScript でチェックされていないコードでも、ランタイムで正しいことが保証されます。- 各クラスインスタンスに、この方法で定義された各関数の独自のコピーがあるため、より多くのメモリを使用します。
- 派生クラスでは
super.getName
を使用できません。これは、基底クラスのメソッドを取得するためのプロトタイプチェーンにエントリがないためです。
this
パラメータ
メソッドまたは関数定義では、this
という名前の最初のパラメータには、TypeScript で特別な意味があります。これらのパラメータはコンパイル中に消去されます。
tsTry
// TypeScript input with 'this' parameterfunctionfn (this :SomeType ,x : number) {/* ... */}
js
// JavaScript outputfunction fn(x) {/* ... */}
TypeScript は、this
パラメータを持つ関数の呼び出しが正しいコンテキストで行われていることをチェックします。アロー関数を使用する代わりに、メソッド定義に this
パラメータを追加して、メソッドが正しく呼び出されることを静的に強制できます。
tsTry
classMyClass {name = "MyClass";getName (this :MyClass ) {return this.name ;}}constc = newMyClass ();// OKc .getName ();// Error, would crashconstg =c .getName ;The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.console .log (g ());
この方法は、アロー関数アプローチの反対のトレードオフを行います。
- JavaScript の呼び出し元は、それを認識せずにクラスメソッドを誤って使用している可能性があります。
- クラスインスタンスごとに1つではなく、クラス定義ごとに1つの関数だけが割り当てられます。
- 基底メソッドの定義は、
super
を介して呼び出すことができます。
this
型
クラスでは、this
という特殊な型は、現在のクラスの型を動的に参照します。これがどのように役立つかを見てみましょう。
tsTry
classBox {contents : string = "";set (value : string) {this.contents =value ;return this;}}
ここで、TypeScript は set
の戻り値の型が Box
ではなく this
であると推論しました。次に、Box
のサブクラスを作成しましょう。
tsTry
classClearableBox extendsBox {clear () {this.contents = "";}}consta = newClearableBox ();constb =a .set ("hello");
パラメータ型注釈で this
を使用することもできます。
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}
これは other: Box
と記述するのとは異なります。派生クラスがある場合、その sameAs
メソッドは、その同じ派生クラスの他のインスタンスのみを受け入れるようになります。
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}classDerivedBox extendsBox {otherContent : string = "?";}constbase = newBox ();constderived = newDerivedBox ();Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.derived .sameAs (); base
this
に基づく型ガード
クラスとインターフェイスのメソッドの戻り位置に this is Type
を使用できます。型絞り込み(例えば、if
ステートメント)と組み合わせると、ターゲットオブジェクトの型は指定された Type
に絞り込まれます。
tsTry
classFileSystemObject {isFile (): this isFileRep {return this instanceofFileRep ;}isDirectory (): this isDirectory {return this instanceofDirectory ;}isNetworked (): this isNetworked & this {return this.networked ;}constructor(publicpath : string, privatenetworked : boolean) {}}classFileRep extendsFileSystemObject {constructor(path : string, publiccontent : string) {super(path , false);}}classDirectory extendsFileSystemObject {children :FileSystemObject [];}interfaceNetworked {host : string;}constfso :FileSystemObject = newFileRep ("foo/bar.txt", "foo");if (fso .isFile ()) {fso .content ;} else if (fso .isDirectory ()) {fso .children ;} else if (fso .isNetworked ()) {fso .host ;}
このベースの型ガードの一般的なユースケースは、特定のフィールドの遅延検証を可能にすることです。例えば、このケースでは、hasValue
が true であることが検証された場合、ボックス内に保持されている値から undefined
が削除されます。
tsTry
classBox <T > {value ?:T ;hasValue (): this is {value :T } {return this.value !==undefined ;}}constbox = newBox <string>();box .value = "Gameboy";box .value ;if (box .hasValue ()) {box .value ;}
パラメータプロパティ
TypeScript は、コンストラクタパラメータを同じ名前と値を持つクラスプロパティに変換するための特別な構文を提供します。これらはパラメータプロパティと呼ばれ、コンストラクタ引数の先頭に可視性修飾子 public
、private
、protected
、または readonly
のいずれかを付けることで作成されます。結果のフィールドには、それらの修飾子が適用されます。
tsTry
classParams {constructor(public readonlyx : number,protectedy : number,privatez : number) {// No body necessary}}consta = newParams (1, 2, 3);console .log (a .x );Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.console .log (a .); z
クラス式
背景資料
クラス式 (MDN)
クラス式は、クラス宣言と非常によく似ています。唯一の実際の違いは、クラス式には名前が必要ないことですが、最終的にバインドされた識別子を介してそれらを参照できます。
tsTry
constsomeClass = class<Type > {content :Type ;constructor(value :Type ) {this.content =value ;}};constm = newsomeClass ("Hello, world");
コンストラクタシグネチャ
JavaScript クラスは、new
演算子でインスタンス化されます。クラス自体の型が与えられた場合、InstanceType ユーティリティ型はこの操作をモデル化します。
tsTry
classPoint {createdAt : number;x : number;y : numberconstructor(x : number,y : number) {this.createdAt =Date .now ()this.x =x ;this.y =y ;}}typePointInstance =InstanceType <typeofPoint >functionmoveRight (point :PointInstance ) {point .x += 5;}constpoint = newPoint (3, 4);moveRight (point );point .x ; // => 8
abstract
クラスとメンバー
TypeScript のクラス、メソッド、およびフィールドは、abstract にすることができます。
抽象メソッドまたは抽象フィールドとは、実装が提供されていないもののことです。これらのメンバーは抽象クラスの中に存在する必要があり、抽象クラスは直接インスタンス化することはできません。
抽象クラスの役割は、すべての抽象メンバーを実装するサブクラスの基本クラスとして機能することです。クラスに抽象メンバーがない場合、そのクラスは具象であると言われます。
例を見てみましょう。
tsTry
abstract classBase {abstractgetName (): string;printName () {console .log ("Hello, " + this.getName ());}}constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.b = newBase ();
Base
は抽象クラスであるため、new
でインスタンス化することはできません。代わりに、派生クラスを作成し、抽象メンバーを実装する必要があります。
tsTry
classDerived extendsBase {getName () {return "world";}}constd = newDerived ();d .printName ();
基本クラスの抽象メンバーを実装し忘れると、エラーが発生することに注意してください。
tsTry
classNon-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.extends Derived Base {// forgot to do anything}
抽象コンストラクトシグネチャ
場合によっては、抽象クラスから派生したクラスのインスタンスを生成するクラスコンストラクター関数を受け入れたいことがあります。
たとえば、次のようなコードを書きたいとします。
tsTry
functiongreet (ctor : typeofBase ) {constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.instance = newctor ();instance .printName ();}
TypeScript は、抽象クラスをインスタンス化しようとしていると正しく指摘しています。結局のところ、greet
の定義が与えられれば、このコードを書くことは完全に合法であり、最終的に抽象クラスを構築することになります。
tsTry
// Bad!greet (Base );
代わりに、コンストラクトシグネチャを持つ何かを受け入れる関数を書きたいのです。
tsTry
functiongreet (ctor : new () =>Base ) {constinstance = newctor ();instance .printName ();}greet (Derived );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.2345Argument 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.greet (); Base
これで TypeScript は、どのクラスコンストラクター関数を呼び出すことができるかを正しく指摘します。Derived
は具象なので呼び出すことができますが、Base
はできません。
クラス間の関係
ほとんどの場合、TypeScript のクラスは、他の型と同じように構造的に比較されます。
たとえば、これら 2 つのクラスは同一であるため、互いに代用して使用できます。
tsTry
classPoint1 {x = 0;y = 0;}classPoint2 {x = 0;y = 0;}// OKconstp :Point1 = newPoint2 ();
同様に、明示的な継承がない場合でも、クラス間のサブタイプ関係が存在します。
tsTry
classPerson {name : string;age : number;}classEmployee {name : string;age : number;salary : number;}// OKconstp :Person = newEmployee ();
これは単純に聞こえますが、他のものよりも奇妙に見えるケースがいくつかあります。
空のクラスにはメンバーがありません。構造的な型システムでは、メンバーを持たない型は一般的に他のすべての型のスーパータイプです。したがって、空のクラスを記述した場合(記述しないでください!)、他のすべてをその代わりに使用できます。
tsTry
classEmpty {}functionfn (x :Empty ) {// can't do anything with 'x', so I won't}// All OK!fn (window );fn ({});fn (fn );