インターフェース
- イントロダクション
- 初めてのインターフェース
- 任意のプロパティ
- 読み込み専用プロパティ
- 過剰プロパティのチェック
- Function型
- インデックス可能な型
- Class型
- インターフェースの拡張
- ハイブリッド型
- インターフェースによるクラスの拡張
イントロダクション
TypeScriptの核となる基本原則のひとつに、値の型チェックが値が持つ形状に焦点を当てていることがあげられます。 これは、時には"ダックタイピング"または"構造的部分型"と呼ばれます。 TypeScriptでは、インターフェースはこれらの型の名付けの規則を満たし、 また、プロジェクトの外観を構成するだけでなく、コードの構造を定義する強力な方法になります。
初めてのインターフェース
インターフェースがどのように動作するのかを、簡単な例で確認してみましょう。
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
型チェッカーは、printLabel呼び出しをチェックします。
printLabel関数はひとつの引数を持ち、
labelと呼ばれる文字列型のプロパティを持つオブジェクトが渡されることを必要とします。
実際のオブジェクトはこれより多くのプロパティを持ちますが、 コンパイラは必要とされている最低限のひとつのプロパティが存在し、 それが必要とされている型とマッチしていることしかチェックしないことに注意してください。 TypeScriptを寛大にさせないように、こちらで少しカバーすることになるケースが存在します。
同じ例を、文字列のlabelプロパティを持つことを必須とするインターフェースを使用して、書きなおしてみましょう。
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
LabelledValueインターフェースは、先の例で必要とされていたものを表現するのに使用できる名前になります。
ここでも依然として、labelと呼ばれる文字列型の単一のプロパティを持つことが表されます。
printLabelに渡すこのオブジェクトが、他の言語でそうする必要があるように、
このインターフェースを実装すると明確に記述していない事に注意してください。
ここでは、事柄を形作っているだけに過ぎません。
関数に渡すオブジェクトが必須となるものを揃えてさえいれば、それは問題なく受け入れられます。
重要な点は、型チェッカーがこれらのプロパティに対して正しい順序であることを必要とせず、 インターフェースで指定されているプロパティが提供され、必須となる型を持っているかだけを必要としていることです。
任意のプロパティ
インターフェースの全てのプロパティを必須にする必要はありません。 幾つかのものはある条件下でのみ存在し、または全く存在しないようにすることも可能です。 これら任意のプロパティは、 関数に必要なプロパティだけを持たせたオブジェクトを渡す際の、 "option bags"のようなパターンを作る際によく使用されます。
下記はこのパターンの例になります。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
任意のプロパティを持つインターフェースの書き方は、他の言語のインターフェースの書き方に似ています。
宣言の中で任意にしたいプロパティの末尾に?の印を付けます。
任意のプロパティの利点は、利用できるプロパティを言い表し、
インターフェースの一部では無いプロパティが使用されることを防ぐ役割も果たしてくれます。
例えば、createSquareのcolorプロパティの名前を打ち間違えたとしても、
エラーメッセージがそのことをすぐに知らせてくれます。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.color) {
// Error: Property 'collor' does not exist on type 'SquareConfig'
newSquare.color = config.collor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
読み込み専用プロパティ
オブジェクトが作成される最初の時には、いくつかのプロパティは編集可(modifiable)のはずです。
プロパティ名の前にreadonlyを置くことで、読み込み専用の指定をすることが可能です。
interface Point {
readonly x: number;
readonly y: number;
}
Pointをオブジェクトリテラルを割り当てて作成することができますが、
その割当後にxとyを変更することはできません。
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScriptのReadonlyArray<T>の型は、
変更処理を行う全てのメソッドが削除されたArray<T>と同義です。
そのため、配列の作成後に変更できないことを確認できます。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
上記の例の最後の行で、ReadonlyArray全体を通常の配列に戻すことさえ、許可されないことがわかります。
それでも、型注釈(type assertion)で上書きすることは可能です。
a = ro as number[];
readonly vs const
readonlyまたはconstのどちらを使用するのかを覚える最も簡単な方法は、
これは変数で使用するのかプロパティで使用するのかを確認することです。
変数であればconstを使用し、プロパティであればreadonlyを使用します。
過剰プロパティのチェック
インターフェースを使った最初の例として、
{ label: string; }のみが期待されているところに、
{ size: number; label: string; }を渡してみましょう。
また、任意のプロパティと、"option bags"がどのように便利かも学んでいきましょう。
しかしながら、安易に両者を繋げてしまうことは、JavaScriptでしていたかもしれないように、
自分の首をしめてしまうことになりかねません。
例えば、直近のcreateSquareを使用した例で確認してみましょう。
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
ここでcreateSquareに渡される引数が、
colorの代わりにcolourとなっていることに注目してください。
素のJavaScriptであれば、このようなケースは沈黙的な失敗となるでしょう。
widthプロパティには適合しており、
colorプロパティは提供されず、過剰なcolourプロパティは何の影響も与えないことから、
あなたは「このプログラムは問題ない」と判定されるのではないかと考えるかもしれません。
しかしながら、TypeScriptは「このコードはもしかしたらバグなのかもしれない」、という判定をくだします。 オブジェクトリテラルは、他の変数へ割り当てられる際に、または引数として渡される際に、 過剰なプロパティが存在しないか特別なチェックを受けます。 もしオブジェクトリテラルが幾つかのプロパティを持ち、 それが"対象の型"を持たない場合はエラーが発生します。
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
このチェックを回避するのは非常に簡単です。 最も簡単な方法は、型注釈(type assertion)を使用することです。
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
ただし、特別なことに使用される幾つかの拡張(extra)プロパティを、オブジェクトが持つことを分かっていれば、
文字列インデックスシグネチャを追加する方が、より良い方法かもしれません。
もし、SquareConfigsがcolorとwidthプロパティを下記の型として持ち、
他の任意の数のプロパティも持つことを可能にするには、次のように定義します。
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
インデックスシグネチャについては後ほど説明しますが、
ここではSquareConfigはcolorまたはwidthではない任意のプロパティを持つことが可能であり、
これらの型は問わない(any)ということだけ説明しておきます。
最後に少し驚かれるかもしれませんが、これらのチェックの回避方法として、
別の変数へオブジェクトを割り当てる、という方法について説明します。
squareOptionsは過剰なプロパティに対するチェックを受けないため、
コンパイラはエラーを発生させません。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
上記のようなシンプルなコードであれば、これらのチェックの"回避"を行う必要は、 おそらく無いであろうことは肝に命じておいてください。 メソッドと状態を保持するような、より複雑なオブジェクトリテラルのために、 これらのテクニックを覚えておくとよいかもしれませんが、 過剰なプロパティのエラーは実際のところ大半がバグでしょう。
これは、option bagsなどに対する過剰プロパティの問題のチェックを実行する場合、
あなたの型宣言の一部を修正する必要があるかもしれないことを意味します。
この場合、もしcreateSquareにcolorとcolourの両方を持つオブジェクトを渡すことが問題無いのであれば、
これを反映するためのSquareConfigの定義を取り付けるべきです。
Function型
インターフェースには、JavaScriptオブジェクトがとり得る型の概要を記述することが可能です。 プロパティを持つオブジェクトを記述することに加え、 インターフェースは関数の型を記述することも可能です。
インターフェースを使用して関数型を記述するために、インターフェースにコールシグネチャを与えます。 これはパラメーターの一覧と戻り値の型のみが指定された関数宣言のようなものです。 各パラメーターは名前と型の両方を必要とします。
interface SearchFunc {
(source: string, subString: string): boolean;
}
定義すれば、他のインターフェースのようにこの関数型を使用することが可能になります。 ここで、関数型の変数をどのように作成し、同じ型の関数の値をどのように適用するのかをお見せします。
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
if (result == -1) {
return false;
}
else {
return true;
}
}
関数型の型のチェックにおいて、パラメーターの名前は一致する必要はありません。 例えば、上記の例を下記のように書くことも可能です。
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
関数のパラメーターは、その位置に対するそれぞれの型に対してひとつずつチェックされます。
全ての型を指定したくない場合、
関数の値はSearchFunc型の変数に直接割り当てられるため、
TypeScriptによるコンテキストの型付けで、引数の型が推測されます。
また、関数式の戻り値の型も、返される値(ここではfalseとtrue)によって暗黙的に決められます。
仮に関数式が数値または文字列を返すとしたら、
型チェッカーは戻り値の型がSearchFuncインターフェースで記述されている型と一致しないという警告を発します。
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
インデックス可能な型
関数型を記述するインターフェースが使用できるのと同様に、
a[10]またはageMap["daniel"]のようなインデックス可能な型を記述することも可能です。
インデックス可能な型はインデックスシグネチャを持ち、これはインデックスされた際に、
対応する戻り値の型と連動して、オブジェクトにインデックスを使用できる型であることを表します。
例を確認してみましょう。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上記で、インデックスシグネチャを持つStringArrayを定義しています。
ここでのStringArrayのインデックスシグネチャの状態は、数値によってインデックス化され、文字列を返すようになっています。
インデックスシグネチャは、文字列(string)と数値(number)の2つの型がサポートされます。 インデックスには両方の型のサポートが可能ですが、 数値インデックスから返される値の型は、文字列インデックスから返される値の型の部分型(subtype)でなければいけません。
何故なら、数値によるインデックス化を行った場合、 JavaScriptは実際にはオブジェクトへのインデックス化を行う前に、それを文字列に変換するからです。 つまり、(数値の)100のインデックス化は、(文字列の)"100"でインデックス化したことと同義であるため、 両者には一貫性がなければいけません。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// Error: indexing with a 'string' will sometimes get you a Dog!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
文字列インデックスシグネチャは"dictionary"(辞書)パターンを表す方法として強力である一方、
全てのプロパティをそれらの戻り値の型と一致させることを強制します。
何故なら、文字列インデックスの宣言obj.propertyは、obj["property"]としても利用できるからです。
下記の例では、nameの型は文字列indexの方と一致しないため、
型チェッカーはエラーを発生させます。
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
}
最後に、割り当てを防ぐために、インデックスシグネチャを読み取り専用にする方法を紹介します。
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
インデックスシグネチャが読み取り専用であるため、myArray[2]へ割り当てすることはできません。
Class型
インターフェースの実装
C#とJavaのような言語での最も一般的なインターフェースの使われ方のひとつに、 クラスが明示的に特定の条件を満たすことを強制させるというものがあり、それはTypeScriptでも可能です。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
クラスに実装するインターフェース内のメソッドに対しての記述も可能であるため、
下記の例ではsetTimeに対してそれを行っています。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
インターフェースにはクラスのpublic、privateの両方ではなく、publicだけが記述されます。 これは、クラスのインスタンスのprivateに、クラスが特定の型を持たせているかチェックすることを禁止します。
クラスの静的側とインスタンス側の違い
クラスとインターフェースを使用することで、 クラスが2つの型、「静的側の型」と「インスタンス側の型」を持つことを意識させる手助けとなってくれます。 あなたは、constructシグネチャ(constructor)のあるインターフェースを作成し、このインターフェースを実装したクラスを作成しようとすると、 エラーが発生することに気づいているかもしれません。
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
これは、クラスがインターフェースを実装した場合、クラスのインスタンス側だけがチェックされるためです。 constructorは静的側であるため、このチェック対象に含まれません。
代わりに、クラスの静的側に直接それを行う必要があります。
この例では、2つのインターフェースを定義し、
ClockConstructorはconstructorのためのもので、
ClockInterfaceはインスタンスメソッドのためのものになります。
ここでは便宜上のため、渡された型のインスタンスを作成するconstructor関数createClockを定義しています。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
createClockのひとつめの引数はClockConstructor型であるため、
createClock(AnalogClock, 7, 32)であれば、
AnalogClockが正しいconstructorシグネチャを持っているかチェックされます。
インターフェースの拡張
クラスのように、インターフェースは拡張することが可能です。 これにより、あるインターフェースのメンバを別のものにコピーすることが可能になり、 インターフェースを再利用性のある部品へ柔軟に分離できるようにしてくれます。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
インターフェースは複数のインターフェースを拡張することが可能で、全てのインターフェースを結合したものを作成します。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
ハイブリッド型
これまで述べてきたように、インターフェースは実際の世界のJavaScriptが提供する豊富な型を記述(説明)することを可能にします。 JavaScriptは動的且つ柔軟な性質を持つことから、あなたは前述された型の組み合わせで動作するオブジェクトを見かけたことがあるかもしれません。
下記はそのような例のひとつになります。 特定のプロパティによって、関数としてもオブジェクトとしても振る舞うオブジェクトになります。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
インターフェースによるクラスの拡張
インターフェースの型がクラスの型を拡張する場合、クラスのメンバを継承しますが、その実装の中身は継承されません。 これは、そのインターフェースが、提供される実装を抜きに、そのクラスの全メンバ宣言を持っているかのように振る舞います。
インターフェースは基盤となるクラスのprivateとprotectedメンバさえも継承します。 これは、privateとprotectedのメンバを持つクラスを拡張したインターフェースを作成する際は、 インターフェースの型はそのクラス、またはそのクラスのサブクラスによってのみ実装可能であることを意味します。
これは、大規模な継承階層を持つが、 コードは特定のプロパティを持つサブクラスのみで動作するように指定したい場合に便利です。 サブクラスは、基底クラスからの継承以外の関連付けは不要です。 例えば、
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control {
select() { }
}
class TextBox extends Control {
select() { }
}
class Image extends Control {
}
class Location {
select() { }
}
上記の例では、SelectableControlはprivateのstateプロパティを含め、
Controlの全てのメンバを含みます。
stateはprivateのメンバであるため、
これだけがControlの子孫としてSelectableControlに実装されることが可能です。
何故なら、Controlの子孫のみが同じ宣言から生じたprivateなstateメンバを持ち、
互換性のためにprivateなメンバであることが必要とされるからです。
Controlクラスにおいては、
SelectableControlのインスタンスを通してprivateメンバであるstateにアクセスすることが可能です。
実際には、SelectableControlがselectメソッドを持つControlのように振る舞います。
ButtonとTextBoxはSelectableControlの部分型(subtype)ですが
(どちらもControlを継承し、selectメソッドを持つため)、
ImageとLocationはそうではありません。
© https://github.com/Microsoft/TypeScript-Handbook
このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- ドキュメントの情報が古い可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。