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

readonlyconst の使い分けはプロパティが 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 インターフェイスはインデックスシグネチャを持ちます。そのインデックスシグネチャStringArraynumber で索引された時は string が買えることを意味します。

インデックスシグネチャは二つの型をサポートしています。stringnumber です。 両方の型のインデックスを使うことはできるのですが、一つ条件があります。"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

インターフェイスがクラスを継承する場合、クラスのメンバーは継承しますが実装は継承されません。それははまるでインターフェイスが実装を提供することなくクラスのメンバーを宣言するかのようです。インターフェイスは 親のクラスのprivateprotected なメンバーまで継承します。これは privateprotected なメンバーを持つクラスを継承したインターフェイスを作成する場合、そのインターフェイス型はそのクラスかそのクラスの子のクラスからしか実装できなくなることを意味します。

これは巨大な継承ヒエラルキーがあるけど特定のプロパティを持つ子クラスのみを扱いたいときに便利です。その子クラスは 親クラスから継承する以外は関連している必要はありません。

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 {

}

上記の例では、SelectableControlControlprivatestate プロパティを含むすべてのメンバを包含しています。stateprivate なメンバなのでControl の子孫しかSelectableControl を実装することができません。なぜなら、Control の子孫だけが 同じ宣言という共通点を持つprivatestate メンバを持っていることになるからです。これはprivate なメンバが互換可能になるために必須です。

Control クラス内では SelectableControl から private なメンバのstateにアクセスすることができます。 SelectableControl は 効果的にselect メソッドを持つことを知っている Control のようにふるまいます。ButtonTextBox クラスは SelectableControl の子クラス(Control から継承し、select メソッドを持っているため)だけれど、Image クラスと Location クラスはそうではありません。