TypeScriptの核となる原則の1つは、型チェックが値の形状に焦点を合わせていることです。これは、「ダックタイピング」または「構造的部分型」と呼ばれることもあります。TypeScriptでは、インターフェースはこれらの型に名前を付ける役割を果たし、コード内およびプロジェクト外のコードとのコントラクトを定義するための強力な方法です。
最初のインターフェース
インターフェースの仕組みを理解する最も簡単な方法は、簡単な例から始めることです。
tsTry
functionprintLabel (labeledObj : {label : string }) {console .log (labeledObj .label );}letmyObj = {size : 10,label : "Size 10 Object" };printLabel (myObj );
型チェッカーは`printLabel`の呼び出しをチェックします。`printLabel`関数は、渡されたオブジェクトに`string`型の`label`というプロパティがあることを要求する単一のパラメーターを持ちます。実際には、オブジェクトはこのパラメーターよりも多くのプロパティを持っていますが、コンパイラーは、少なくとも必要なプロパティが存在し、必要な型と一致しているかどうかのみをチェックすることに注意してください。TypeScriptがそれほど寛容でない場合もありますが、それについては後で説明します。
同じ例をもう一度書くことができます。今回は、`string`型の`label`プロパティを持つという要件を記述するためにインターフェースを使用します。
tsTry
interfaceLabeledValue {label : string;}functionprintLabel (labeledObj :LabeledValue ) {console .log (labeledObj .label );}letmyObj = {size : 10,label : "Size 10 Object" };printLabel (myObj );
インターフェース`LabeledValue`は、前の例の要件を記述するために使用できる名前です。これは、`string`型である`label`という単一のプロパティを持つことを表しています。他の言語のように、`printLabel`に渡すオブジェクトがこのインターフェースを実装していると明示的に言う必要がなかったことに注意してください。ここでは、形状だけが重要です。関数に渡すオブジェクトがリストされている要件を満たしていれば、許可されます。
型チェッカーは、これらのプロパティが何らかの順序で提供されることを要求せず、インターフェースが必要とするプロパティが存在し、必要な型を持っていることのみを要求することに注意してください。
オプションのプロパティ
インターフェースのすべてのプロパティが必須であるわけではありません。特定の条件下でのみ存在するものや、まったく存在しないものもあります。これらのオプションのプロパティは、「オプションバッグ」のようなパターンを作成する場合に役立ちます。このパターンでは、いくつかのプロパティのみが入力されたオブジェクトを関数に渡します。
このパターンの例を次に示します。
tsTry
interfaceSquareConfig {color ?: string;width ?: number;}functioncreateSquare (config :SquareConfig ): {color : string;area : number } {letnewSquare = {color : "white",area : 100 };if (config .color ) {newSquare .color =config .color ;}if (config .width ) {newSquare .area =config .width *config .width ;}returnnewSquare ;}letmySquare =createSquare ({color : "black" });
オプションのプロパティを持つインターフェースは、他のインターフェースと同様に記述されます。各オプションのプロパティは、宣言のプロパティ名の末尾に`?`を付けて表します。
オプションのプロパティの利点は、これらの潜在的に使用可能なプロパティを記述しながら、インターフェースの一部ではないプロパティの使用を防ぐことができることです。たとえば、`createSquare`で`color`プロパティの名前を間違えて入力した場合、それを知らせるエラーメッセージが表示されます。
tsTry
interfaceSquareConfig {color ?: string;width ?: number;}functioncreateSquare (config :SquareConfig ): {color : string;area : number } {letnewSquare = {color : "white",area : 100 };if (Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?config .) { clor // Error: Property 'clor' does not exist on type 'SquareConfig'Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?newSquare .color =config .; clor }if (config .width ) {newSquare .area =config .width *config .width ;}returnnewSquare ;}letmySquare =createSquare ({color : "black" });
読み取り専用プロパティ
オブジェクトが最初に作成されたときのみ変更可能なプロパティがあります。プロパティ名の前にreadonly
を付けることで、これを指定できます。
tsTry
interfacePoint {readonlyx : number;readonlyy : number;}
オブジェクトリテラルを代入することで、Point
を構築できます。代入後、x
とy
は変更できません。
tsTry
letp1 :Point = {x : 10,y : 20 };Cannot assign to 'x' because it is a read-only property.2540Cannot assign to 'x' because it is a read-only property.p1 .= 5; // error! x
TypeScriptには、すべての変更メソッドが削除されたArray<T>
と同じであるReadonlyArray<T>
型が付属しているため、作成後に配列が変更されないようにすることができます。
tsTry
leta : number[] = [1, 2, 3, 4];letro :ReadonlyArray <number> =a ;Index signature in type 'readonly number[]' only permits reading.2542Index signature in type 'readonly number[]' only permits reading.ro [0] = 12; // error!Property 'push' does not exist on type 'readonly number[]'.2339Property 'push' does not exist on type 'readonly number[]'.ro .(5); // error! push Cannot assign to 'length' because it is a read-only property.2540Cannot assign to 'length' because it is a read-only property.ro .= 100; // error! length The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.4104The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.= a ro ; // error!
スニペットの最後の行では、ReadonlyArray
全体を通常の配列に代入することさえできないことがわかります。ただし、型アサーションでオーバーライドすることはできます。
tsTry
leta : number[] = [1, 2, 3, 4];letro :ReadonlyArray <number> =a ;a =ro as number[];
readonly
vs const
readonly
またはconst
を使用するかどうかを覚える最も簡単な方法は、変数またはプロパティのどちらで使用しているかを確認することです。変数はconst
を使用し、プロパティはreadonly
を使用します。
過剰プロパティチェック
インターフェースを使用した最初の例では、TypeScriptでは、{ label: string; }
のみを想定しているものに{ size: number; label: string; }
を渡すことができます。また、オプションのプロパティと、いわゆる「オプションバッグ」を記述する際にどのように役立つかについても学びました。
ただし、2つを単純に組み合わせると、エラーが忍び込む可能性があります。たとえば、createSquare
を使用した最後の例を見てみましょう。
tsTry
interfaceSquareConfig {color ?: string;width ?: number;}functioncreateSquare (config :SquareConfig ): {color : string;area : number } {return {color :config .color || "red",area :config .width ?config .width *config .width : 20,};}letArgument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2345Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?mySquare =createSquare ({colour : "red",width : 100 });
createSquare
に渡された引数が、color
ではなくcolour
と綴られていることに注意してください。プレーンJavaScriptでは、この種のことは静かに失敗します。
width
プロパティは互換性があり、color
プロパティが存在せず、追加のcolour
プロパティは重要ではないため、このプログラムは正しく型指定されていると主張することもできます。
ただし、TypeScriptはこのコードにバグがある可能性があると判断します。オブジェクトリテラルは特別な扱いをされ、他の変数に代入したり、引数として渡したりするときに、*過剰プロパティチェック*が行われます。オブジェクトリテラルに「ターゲット型」にないプロパティがある場合、エラーが発生します。
tsTry
letArgument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2345Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?mySquare =createSquare ({colour : "red",width : 100 });
これらのチェックを回避するのは実際には非常に簡単です。最も簡単な方法は、型アサーションを使用することです。
tsTry
letmySquare =createSquare ({width : 100,opacity : 0.5 } asSquareConfig );
ただし、オブジェクトが特別な方法で使用される追加のプロパティを持つことができることが確実な場合は、文字列インデックスシグネチャを追加する方が良い方法かもしれません。SquareConfig
が上記の型を持つcolor
およびwidth
プロパティを持つことができ、*さらに*他のプロパティをいくつでも持つことができる場合は、次のように定義できます。
tsTry
interfaceSquareConfig {color ?: string;width ?: number;[propName : string]: any;}
インデックスシグネチャについては後で説明しますが、ここでは、SquareConfig
は任意の数のプロパティを持つことができ、それらがcolor
またはwidth
でない限り、それらの型は重要ではないと言っています。
これらのチェックを回避する最後の方法は、少し驚くかもしれませんが、オブジェクトを別の変数に代入することです。squareOptions
は過剰プロパティチェックを受けないため、コンパイラはエラーを出しません。
tsTry
letsquareOptions = {colour : "red",width : 100 };letmySquare =createSquare (squareOptions );
上記の回避策は、squareOptions
とSquareConfig
の間に共通のプロパティがある限り機能します。この例では、それはプロパティwidth
でした。ただし、変数に共通のオブジェクトプロパティがない場合は失敗します。例えば
tsTry
letsquareOptions = {colour : "red" };letType '{ colour: string; }' has no properties in common with type 'SquareConfig'.2559Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.mySquare =createSquare (); squareOptions
上記のような単純なコードの場合、これらのチェックを「回避」しようとするべきではないことに注意してください。メソッドを持ち、状態を保持するより複雑なオブジェクトリテラルの場合、これらの手法を覚えておく必要があるかもしれませんが、過剰プロパティエラーの大部分は実際にはバグです。つまり、オプションバッグのようなものの過剰プロパティチェックの問題が発生している場合は、型宣言の一部を修正する必要があるかもしれません。このインスタンスでは、color
またはcolour
プロパティの両方を持つオブジェクトをcreateSquare
に渡すことが問題ない場合は、SquareConfig
の定義を修正してそれを反映する必要があります。
関数型
インターフェースは、JavaScriptオブジェクトがとることができる幅広い形状を記述できます。プロパティを持つオブジェクトを記述することに加えて、インターフェースは関数型を記述することもできます。
インターフェースで関数型を記述するには、インターフェースに呼び出しシグネチャを指定します。これは、パラメータリストと戻り値の型のみが指定された関数宣言のようなものです。パラメータリストの各パラメータには、名前と型の両方が必要です。
tsTry
interfaceSearchFunc {(source : string,subString : string): boolean;}
定義したら、この関数型インターフェースを他のインターフェースと同様に使用できます。ここでは、関数型の変数を作成し、それに同じ型の関数値を代入する方法を示します。
tsTry
letmySearch :SearchFunc ;mySearch = function (source : string,subString : string): boolean {letresult =source .search (subString );returnresult > -1;};
関数型が正しく型チェックされるためには、パラメータの名前が一致する必要はありません。たとえば、上記の例は次のように書くこともできました。
tsTry
letmySearch :SearchFunc ;mySearch = function (src : string,sub : string): boolean {letresult =src .search (sub );returnresult > -1;};
関数パラメータは一度に1つずつチェックされ、対応する各パラメータ位置の型が互いにチェックされます。型をまったく指定したくない場合は、関数値が型SearchFunc
の変数に直接代入されるため、TypeScriptのコンテキスト型指定で引数の型を推論できます。ここでも、関数式の戻り値の型は、それが返す値(ここではfalse
とtrue
)によって暗示されます。
tsTry
letmySearch :SearchFunc ;mySearch = function (src ,sub ) {letresult =src .search (sub );returnresult > -1;};
関数式が数値または文字列を返した場合、型チェッカーは、戻り値の型がSearchFunc
インターフェースで記述されている戻り値の型と一致しないことを示すエラーを生成します。
tsTry
letmySearch :SearchFunc ;Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.2322Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.= function ( mySearch src ,sub ) {letresult =src .search (sub );return "string";};
インデックス可能な型
インターフェースを使用して関数型を記述する方法と同様に、a[10]
やageMap["daniel"]
のように「インデックス化できる」型を記述することもできます。インデックス可能な型には、オブジェクトにインデックスを付けるために使用できる型と、インデックス付け時の対応する戻り値の型を記述する*インデックスシグネチャ*があります。
例を見てみましょう。
tsTry
interfaceStringArray {[index : number]: string;}letmyArray :StringArray ;myArray = ["Bob", "Fred"];letmyStr : string =myArray [0];
上記では、インデックスシグネチャを持つStringArray
インターフェースがあります。このインデックスシグネチャは、StringArray
がnumber
でインデックス付けされると、string
を返すことを示しています。
サポートされているインデックスシグネチャには、文字列、数値、シンボル、テンプレート文字列の4種類があります。多くのタイプのインデクサーをサポートすることは可能ですが、数値インデクサーから返される型は、文字列インデクサーから返される型のサブタイプである必要があります。
これは、number
でインデックスを作成する場合、JavaScriptはオブジェクトにインデックスを作成する前に実際にそれをstring
に変換するためです。つまり、100
(number
)でインデックスを作成することは、"100"
(string
)でインデックスを作成することと同じであるため、2つは一致する必要があります。
tsTry
interfaceAnimal {name : string;}interfaceDog extendsAnimal {breed : string;}// Error: indexing with a numeric string might get you a completely separate type of Animal!interfaceNotOkay {['number' index type 'Animal' is not assignable to 'string' index type 'Dog'.2413'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.x : number]:Animal ;[x : string]:Dog ;}
文字列インデックスシグネチャは「辞書」パターンを記述するための強力な方法ですが、すべてのプロパティが戻り値の型と一致することも強制します。これは、文字列インデックスがobj.property
がobj["property"]
としても使用可能であると宣言するためです。次の例では、name
の型は文字列インデックスの型と一致しないため、型チェッカーはエラーを返します。
tsTry
interfaceNumberDictionary {[index : string]: number;length : number; // ok, length is a numberProperty 'name' of type 'string' is not assignable to 'string' index type 'number'.2411Property 'name' of type 'string' is not assignable to 'string' index type 'number'.: string; // error, the type of 'name' is not a subtype of the indexer name }
ただし、インデックスシグネチャがプロパティ型の共用体である場合、異なる型のプロパティは許容されます。
tsTry
interfaceNumberOrStringDictionary {[index : string]: number | string;length : number; // ok, length is a numbername : string; // ok, name is a string}
最後に、インデックスへの代入を防ぐために、インデックスシグネチャをreadonly
にすることができます。
tsTry
interfaceReadonlyStringArray {readonly [index : number]: string;}letmyArray :ReadonlyStringArray = ["Alice", "Bob"];Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.myArray [2] = "Mallory"; // error!
インデックスシグネチャがreadonly
であるため、myArray[2]
を設定できません。
テンプレート文字列を使用したインデックス可能な型
テンプレート文字列を使用して、特定のパターンが許可されるが、すべてではないことを示すことができます。たとえば、HTTPヘッダーオブジェクトには、既知のヘッダーのセットリストがあり、`x-`で始まるカスタム定義プロパティをサポートできます。
tsTry
interfaceHeadersResponse {"content-type": string,date : string,"content-length": string// Permit any property starting with 'x-'.[headerName : `x-${string}`]: string;}functionhandleResponse (r :HeadersResponse ) {// Handle known, and x- prefixedconsttype =r ["content-type"]constpoweredBy =r ["x-powered-by"]// Unknown keys without the prefix raise errorsconstProperty 'origin' does not exist on type 'HeadersResponse'.2339Property 'origin' does not exist on type 'HeadersResponse'.origin =r .origin }
クラス型
インターフェースの実装
C#やJavaなどの言語でインターフェースの最も一般的な用途の1つである、クラスが特定の契約を満たすことを明示的に強制することも、TypeScriptで可能です。
tsTry
interfaceClockInterface {currentTime :Date ;}classClock implementsClockInterface {currentTime :Date = newDate ();constructor(h : number,m : number) {}}
以下の例で`setTime`を使用しているように、クラスで実装されているインターフェースでメソッドを記述することもできます。
tsTry
interfaceClockInterface {currentTime :Date ;setTime (d :Date ): void;}classClock implementsClockInterface {currentTime :Date = newDate ();setTime (d :Date ) {this.currentTime =d ;}constructor(h : number,m : number) {}}
インターフェースは、パブリックとプライベートの両方ではなく、クラスのパブリック側を記述します。これにより、クラスインスタンスのプライベート側にも特定の型があることを確認するために使用することができなくなります。
~クラスの静的側とインスタンス側の違い
クラスとインターフェースを扱う場合、クラスには*2つ*の型があることに注意してください。静的側の型とインスタンス側の型です。コンストラクトシグネチャを持つインターフェースを作成し、このインターフェースを実装するクラスを作成しようとすると、エラーが発生することに気付くかもしれません。
tsTry
interfaceClockConstructor {new (hour : number,minute : number);}classClass 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.2420Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.implements Clock ClockConstructor {currentTime :Date ;constructor(h : number,m : number) {}}
これは、クラスがインターフェースを実装する場合、クラスのインスタンス側のみがチェックされるためです。コンストラクターは静的側に存在するため、このチェックには含まれません。
代わりに、クラスの静的側で直接作業する必要があります。この例では、コンストラクター用の`ClockConstructor`とインスタンスメソッド用の`ClockInterface`の2つのインターフェースを定義します。次に、便宜上、渡された型のインスタンスを作成するコンストラクター関数`createClock`を定義します。
tsTry
interfaceClockConstructor {new (hour : number,minute : number):ClockInterface ;}interfaceClockInterface {tick (): void;}functioncreateClock (ctor :ClockConstructor ,hour : number,minute : number):ClockInterface {return newctor (hour ,minute );}classDigitalClock implementsClockInterface {constructor(h : number,m : number) {}tick () {console .log ("beep beep");}}classAnalogClock implementsClockInterface {constructor(h : number,m : number) {}tick () {console .log ("tick tock");}}letdigital =createClock (DigitalClock , 12, 17);letanalog =createClock (AnalogClock , 7, 32);
`createClock`の最初のパラメーターは`ClockConstructor`型であるため、`createClock(AnalogClock, 7, 32)`では、`AnalogClock`に正しいコンストラクターシグネチャがあることをチェックします。
もう1つの簡単な方法は、クラス式を使用することです。
tsTry
interfaceClockConstructor {new (hour : number,minute : number):ClockInterface ;}interfaceClockInterface {tick (): void;}constClock :ClockConstructor = classClock implementsClockInterface {constructor(h : number,m : number) {}tick () {console .log ("beep beep");}};letclock = newClock (12, 17);clock .tick ();
~インターフェースの拡張
クラスと同様に、インターフェースは互いに拡張できます。これにより、1つのインターフェースのメンバーを別のインターフェースにコピーできるため、インターフェースを再利用可能なコンポーネントに分割する方法に柔軟性が生まれます。
tsTry
interfaceShape {color : string;}interfaceSquare extendsShape {sideLength : number;}letsquare = {} asSquare ;square .color = "blue";square .sideLength = 10;
インターフェースは複数のインターフェースを拡張して、すべてのインターフェースの組み合わせを作成できます。
tsTry
interfaceShape {color : string;}interfacePenStroke {penWidth : number;}interfaceSquare extendsShape ,PenStroke {sideLength : number;}letsquare = {} asSquare ;square .color = "blue";square .sideLength = 10;square .penWidth = 5.0;
~ハイブリッド型
前述のように、インターフェースは現実世界のJavaScriptに存在する豊富な型を記述できます。JavaScriptの動的で柔軟な性質のため、上記の型の組み合わせとして機能するオブジェクトに遭遇することがあります。
そのような例の1つは、関数とオブジェクトの両方として機能し、追加のプロパティを持つオブジェクトです。
tsTry
interfaceCounter {(start : number): string;interval : number;reset (): void;}functiongetCounter ():Counter {letcounter = function (start : number) {} asCounter ;counter .interval = 123;counter .reset = function () {};returncounter ;}letc =getCounter ();c (10);c .reset ();c .interval = 5.0;
サードパーティのJavaScriptと対話する場合、上記のパータンを使用して型の形状を完全に記述する必要がある場合があります。
~クラスを拡張するインターフェース
インターフェース型がクラス型を拡張する場合、クラスのメンバーは継承されますが、実装は継承されません。インターフェースが実装を提供せずにクラスのすべてのメンバーを宣言したかのようです。インターフェースは、基底クラスのプライベートメンバーと保護されたメンバーも継承します。つまり、プライベートメンバーまたは保護されたメンバーを持つクラスを拡張するインターフェースを作成する場合、そのインターフェース型は、そのクラスまたはそのサブクラスによってのみ実装できます。
これは、大きな継承階層があるが、コードが特定のプロパティを持つサブクラスのみで動作するように指定する場合に便利です。サブクラスは、基底クラスから継承する以外に関連付ける必要はありません。次に例を示します。
tsTry
classControl {privatestate : any;}interfaceSelectableControl extendsControl {select (): void;}classButton extendsControl implementsSelectableControl {select () {}}classTextBox extendsControl {select () {}}classClass 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.2420Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.implements ImageControl SelectableControl {privatestate : any;select () {}}
上記の例では、`SelectableControl`には、プライベート`state`プロパティを含む`Control`のすべてのメンバーが含まれています。 `state`はプライベートメンバーであるため、`Control`の子孫のみが`SelectableControl`を実装できます。これは、`Control`の子孫のみが、同じ宣言で発生する`state`プライベートメンバーを持つためです。これは、プライベートメンバーが互換性を持つための要件です。
`Control`クラス内では、`SelectableControl`のインスタンスを介して`state`プライベートメンバーにアクセスできます。事実上、`SelectableControl`は、`select`メソッドを持つことがわかっている`Control`のように動作します。 `Button`クラスと`TextBox`クラスは`SelectableControl`のサブタイプです(どちらも`Control`から継承し、`select`メソッドを持っているため)。 `ImageControl`クラスには、`Control`を拡張するのではなく独自の`state`プライベートメンバーがあるため、`SelectableControl`を実装できません。