Typescript: Interface について調べた
Interface
Typescript の理念は 値が持つ "形" の型チェックに焦点を当てています。このことは、"ダックタイピング" や "構造部分型" などと呼ばれる。Typescript のインターフェイスはその型への名前付けの役割を果たします。
最初の Interface
function printLabel(labeledObj: { label: string }) { console.log(labeledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
上の例でコンパイルすることができる。printaLabel
関数の引数の数より関数呼び出しの方が多く引数として渡しているのになぜコンパイルが通るのでしょうか。それは、コンパイラーは、呼び出し側が、少なくとも必須型として定義されている型 (上記の例でいう { label: string }
)を満たしているかどうかをチェックしているからです。
上記の 型 { label: string }
に名前を付けるなら以下の通りになります。
interface LabeledValue { label: string; } function printLabel(labeledObj: LabeledValue) { console.log(labeledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
Readonly properties
readonly 修飾子のついたメンバは後から変更できません。
interface Point { readonly x: number; readonly y: number; } let p1: Point = { x: 10, y: 20 }; p1.x = 5; // error!
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!
普通の方に readonly
型の値を割り当てるには型アサーションを行います。
a = ro as number[];
readonly vs const
readonly
と const
の使い分けはプロパティが readonly
変数が const
。
過剰プロパティ型チェック
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 'clor' does not exist on type 'SquareConfig' newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } // error Argument 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'? let mySquareA = createSquare({ colour: "red", width: 100 }) let square = { colour: "red", width: 100 } let mySquareB = createSquare(square);
上記の例では mySquareA
に割り当てようとしている関数の呼び出しの引数が 型のメンバ color
ではなく colour
になっている。Typescript はこれをバグとみなし エラーを出してくれます。よく見ると 「もしかしてこれと間違えてる?」といったサジェストまでしてくれます。優しい。
定義してある型メンバ以外の値をあらかじめ予測できる場合は以下のように書く。
interface SquareConfig { color?: string; width?: number; [propName: string]: any; }
ところが、以下ではそのタイポしたオブジェクトを一度変数に代入して、その後その変数の値を引数として関数を呼び出すとエラーとならず呼び出すことができてしまいます。 なぜなら squareOptions
過剰プロパティ型チェックされずにコンパイルを通ってしまうからです。
let squareOptions = { colour: "red", width: 100 }; let mySquare = createSquare(squareOptions);
値に要求されているプロパティが一つもない変数は割り当てることができない。
let squareOptions = { colour: "red" }; let mySquare = createSquare(squareOptions); // error!
Indexable Types
インデックス可能な型はオブジェクトを索引するときに使う型と、索引されたものに合致する戻り型を意味するインデックスシグネチャを持ちます。
interface StringArray { [index: number]: string; } let a: StringArray = ["hello","world"] console.log(a[1]) // => world
上記のStringArray
インターフェイスはインデックスシグネチャを持ちます。そのインデックスシグネチャは StringArray
が number
で索引された時は string
が買えることを意味します。
インデックスシグネチャは二つの型をサポートしています。string
と number
です。 両方の型のインデックスを使うことはできるのですが、一つ条件があります。"number
型のインデクッスからの戻り型は、string
型のインデックスからの戻り型の子の型でなければならない。" ということです。
なぜなら、number
でインデックスを作成する場合、JavaScriptは実際にはオブジェクトにインデックスを作成する前にそれを文字列に変換するためです。つまり、100
(number) は "100" (string) でインデックスを作成することと同じです。
文字列インデックスは 全てのプロパティにインデックスの戻り型に一致するように求めるので、辞書型のパターンを表現するのにぴったりです。
interface NumberDictionary { [index: string]: number; length: number; // ok, length is a number name: string; // error, 'name' のかたは インデックスの子の型ではありません。 }
インデックスに readonly
を付けることもできる。
interface ReadonlyStringArray { readonly [index: number]: string; } let myArray: ReadonlyStringArray = ["Alice", "Bob"]; myArray[2] = "Mallory"; // error!
Class interface
静的クラスとインスタンスクラスの違い
class interface には二つの面があります。静的側面の型と、インスタンス側面の型です。例えば、コンストラクタシグネチャを持つインターフェイスを作って、そのインターフェイスを以下の実装しようとしたらエラーが出ます。
interface ClockConstructor { new (hour: number, minute: number); } class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) { } }
なぜなら、インターフェイスを実装する場合、クラスのインスタンスの側面だけしかチェックされないからです。上記のコンストラクタは静的な領域に止まっているので、このチェックには含まれないのです。
下のように クラスの静的な側面とインスタンスの側面を使い分ける必要があります。
interface ClockConstructor { new (hour: number, minute: number): ClockInterface; } interface ClockInterface { tick(): void; } 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);
また上記の例は以下のようにシンプルに書き直すこともできます。
interface ClockConstructor { new (hour: number, minute: number); } interface ClockInterface { tick(); } const Clock: ClockConstructor = class Clock implements ClockInterface { constructor(h: number, m: number) {} tick() { console.log("beep beep"); } }
Extending Interfaces
クラスと同様にインターフェイスはお互いに拡張することができます。これによりインターフェイスのメンバーを他にコピーすることが可能にまります。
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;
Interfaces extending classes
インターフェイスがクラスを継承する場合、クラスのメンバーは継承しますが実装は継承されません。それははまるでインターフェイスが実装を提供することなくクラスのメンバーを宣言するかのようです。インターフェイスは 親のクラスのprivate
や protected
なメンバーまで継承します。これは private
や protected
なメンバーを持つクラスを継承したインターフェイスを作成する場合、そのインターフェイス型はそのクラスかそのクラスの子のクラスからしか実装できなくなることを意味します。
これは巨大な継承ヒエラルキーがあるけど特定のプロパティを持つ子クラスのみを扱いたいときに便利です。その子クラスは 親クラスから継承する以外は関連している必要はありません。
class Control { private state: any; } interface SelectableControl extends Control { select(): void; } class Button extends Control implements SelectableControl { select() { } } class TextBox extends Control { select() { } } // Error: Property 'state' is missing in type 'Image'. class Image implements SelectableControl { private state: any; select() { } } class Location { }
上記の例では、SelectableControl
はControl
の private
な state
プロパティを含むすべてのメンバを包含しています。state
はprivate
なメンバなのでControl
の子孫しかSelectableControl
を実装することができません。なぜなら、Control
の子孫だけが 同じ宣言という共通点を持つprivate
な state
メンバを持っていることになるからです。これはprivate
なメンバが互換可能になるために必須です。
Control
クラス内では SelectableControl
から private
なメンバのstate
にアクセスすることができます。 SelectableControl
は 効果的にselect
メソッドを持つことを知っている Control
のようにふるまいます。Button
と TextBox
クラスは SelectableControl
の子クラス(Control
から継承し、select
メソッドを持っているため)だけれど、Image
クラスと Location
クラスはそうではありません。