TypeScript インターフェース

TypeScriptのインターフェースは、コードの中でデータ構造を定義するために使われます。

これにより、クラスやオブジェクトが持つべきプロパティやメソッドを指定することができ、実装の詳細は提供されません。これは大規模なコードベースを扱うときや、複数の開発者が使用するコードを開発するときに特に有効です。

TypeScriptのインターフェースは、interfaceキーワードとそれに続くインターフェース名で定義されます。インターフェースが定義するプロパティとメソッドは中括弧で囲み、それぞれのプロパティとメソッドをカンマで区切って列挙します。

例えば、Personオブジェクトにnameプロパティとgreetメソッドを定義する簡単なインターフェースを以下に示します。

interface Person {
    name: string;
    greet(): string;
}

インターフェース型の変数を作成することも可能です。

let person : Person = {
    name: 'John',
    greet: () => 'Hello'
}

console.log(person.name); // "John"
console.log(person.greet()); // "Hello"

TypeScript インターフェースの実装

インターフェースが定義されると、それをクラスで実装することができます。そのためには、クラスがインターフェースで定義されたプロパティとメソッドを含む必要があり、クラスが宣言されるときにimplementsキーワードを使用する必要があります。

例えば、Personインターフェースを実装した簡単なクラスは次のとおりです。

interface Person {
    name: string;
    greet(): string;
}

class MyPerson implements Person {
    name: string;

    constructor(name: string) {
        this.name = name;
        }

    greet(): string {
        return "Hello, my name is " + this.name;
    }
}

let person1: Person = new MyPerson("John Lake");
console.log(person1.name); // "John Lake"
console.log(person1.greet()); // "Hello, my name is John Lake"

let person2: Person = new MyPerson("Jane Smith");
console.log(person2.name); // "Jane Smith"
console.log(person2.greet()); // "Hello, my name is Jane Smith"

この例では、person1とperson2はPerson型の変数です。どちらもPersonインターフェースを実装したMyPersonクラスのインスタンスとして代入されています。MyPersonクラスにはコンストラクタがあり、nameという文字列の引数を受け取り、クラスのnameプロパティに代入します。greetメソッドも実装されており、nameプロパティの値を含む文字列を返します。

それでは、他の例も見てみましょう。

interface Shape {
    getArea(): number;
}

class Rectangle implements Shape {
    width: number;
    height: number;
    constructor(width: number, height: number) {
        this.width = width;
        this.height = height;
    }
    getArea(): number {
        return this.width * this.height;
    }
}

class Circle implements Shape {
    radius: number;
    constructor(radius: number) {
        this.radius = radius;
    }
    getArea(): number {
        return this.radius * this.radius * Math.PI;
    }
}

let rectangle: Shape = new Rectangle(5, 10);
let circle: Shape = new Circle(5);

console.log(rectangle.getArea()); // 50
console.log(circle.getArea()); // 78.53981633974483

この例では、インターフェースShapeがあり、数値を返すgetArea()メソッドが1つ定義されています。そして、Shapeインターフェースを実装した2つのクラス、RectangleとCircleがあります。Rectangleクラスはwidthとheightのプロパティを持ち、長方形の面積を返すgetArea()メソッドを実装しています。Circleクラスは半径のプロパティを持ち、円の面積を返すgetArea()メソッドを実装しています。

RectangleとCircleのインスタンスを作成し、それぞれ変数rectangleとcircleに代入します。どちらのクラスもShapeインターフェースを実装しているので、互換的に使用でき、両方のクラスでgetArea()メソッドを呼び出すことができます。

この例では、クラスの構造を定義するためにインターフェースを使用し、クラスがそのインターフェースを実装して特定の機能を提供する方法を示しています。インターフェースを使うことで、特定のインターフェースを実装するすべてのクラスが同じメソッド群を持つことができ、一貫した方法で作業することが容易になります。

TypeScript インターフェースの拡張

extendsキーワードを使用すると、インターフェースを拡張することもできます。既存のインターフェースのプロパティやメソッドをすべて継承した上で、新しいプロパティやメソッドを追加した新しいインターフェースを作成することができます。

例えば、Personインターフェースを拡張して、ageプロパティを追加したインターフェースを以下に示します。

interface Person {
    name: string;
    greet(): string;
}

interface Adult extends Person {
    age: number;
}

class MyPerson implements Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    greet(): string {
        return "Hello, my name is " + this.name;
    }
}

class MyAdult extends MyPerson implements Adult {
    age: number;
    constructor(name: string, age: number) {
        super(name);
        this.age = age;
    }
}

let person: Person = new MyPerson("John");
console.log(person.name); // John
console.log(person.greet()); // "Hello, my name is John"

let adult: Adult = new MyAdult("Jane", 30);
console.log(adult.greet()); // "Hello, my name is Jane"
console.log(adult.age); // 30

この例では、Personインターフェースを実装したクラスMyPersonを用意しました。インターフェースに従って、nameプロパティとgreetメソッドを定義しています。MyPersonを継承し、Adultインターフェースを実装したMyAdultクラスもあります。MyAdultクラスはAdultインターフェースで定義されているageというプロパティを追加で持っています。

MyPersonとMyAdultのインスタンスを作成し、それぞれpersonとadult変数に代入します。インターフェースで定義されたプロパティやメソッドにアクセスすることができます。

superキーワードは、サブクラスのコンストラクタで、親クラスのコンストラクタを呼び出すために使用されます。上の例では、MyAdultクラスはMyPersonクラスを継承していますが、このクラスはすでにPersonインターフェースを実装しています。

Personインターフェースで定義されたプロパティやメソッドをMyAdultクラスで使用するには、MyAdultクラスがMyPersonクラスのコンストラクタをsuperキーワードを使用して呼び出し、nameプロパティが適切に初期化されていることを確認する必要があります。

TypeScript インターフェースのオーバーライド

TypeScriptでは、インターフェースを拡張する際に、元のインターフェースですでに定義されているプロパティのデータ型を変更すると、コンパイル時にエラーになります。これは、拡張されたインターフェースが有効な実装とみなされるためには、元のインターフェースと完全に互換性がなければならないからです。

interface Person {
    name: string;
    age: number;
}

interface Adult extends Person {
    age: string; // error
}

この例では、AdultインターフェースはPersonインターフェースを継承していますが、ageプロパティのデータ型が数値から文字列に変更されています。これは、AdultインターフェースのageプロパティがPersonインターフェースの元のageプロパティと完全に互換性がないため、コンパイル時エラーになります。

インターフェースを拡張する場合、追加または変更されたプロパティやメソッドは、元のインターフェースと完全に互換性がなければならないことを覚えておくことが重要です。これにより、元のインターフェースを実装しているクラスやオブジェクトは、拡張されたインターフェースが期待される場所で問題なく使用することができるようになります。

TypeScript インターフェースのマージ

TypeScriptでは、同じスコープに同じ名前のインターフェースが複数ある場合、それらは自動的にマージされます。つまり、あるインターフェースで定義されたプロパティやメソッドは、他のインターフェースで定義されたものと統合され、ひとつの統一されたインターフェースとなります。

interface Person {
    name: string;
}

interface Person {
    age: number;
}

let person: Person = { name: 'John', age: 30 }
console.log(person) // { name: 'John', age: 30 }

この例では、Personという同じ名前の2つのインターフェースがあり、 両インターフェースともnameとageというそれぞれのプロパティを持っています。この2つのインターフェースが同じスコープで宣言されると、自動的にマージされ、nameプロパティとageプロパティの両方を持つ1つのインターフェースが作成されます。

注意すべきは、同じ名前のインターフェースが複数あり、それらが重複するプロパティを持つ場合、最後に定義されたインターフェースが優先されることです。

また、インターフェースのマージは便利な場合もありますが、混乱を招き、コードの理解や保守が困難になる可能性があることも知っておくとよいでしょう。一般的には、インターフェースの統合は避け、各インターフェースに一意な名前を使うのがベストです。

TypeScript 型エイリアスとインターフェース

TypeScriptでは、typeキーワードとinterfaceキーワードの両方を用いて新しい型を作成することができますが、その使い方や最適な用途にはいくつかの違いがあります。

インターフェースは、オブジェクトの形状を記述する新しい型を定義するために使用されます。オブジェクトが持つべきプロパティやメソッド、イベントなどを定義することができ、他のインターフェースやクラスで拡張したり実装したりすることも可能です。

一方、typeキーワードはより汎用的な構成要素で、様々な方法で新しい型を定義するために使用されます。また、既存の型の新しい名前である新しい型のエイリアスを作成するために使用することができます。他の型の論理和や論理積となる新しい型を作成したり、既存の型に一連のルールを適用して新しい型を作成するマッピングされた型を作成するためにも使用できます。

以下は、typeキーワードとinterfaceキーワードを使用して新しい型を作成する例です。PersonTypeとPersonInterfaceの両方が、nameプロパティが文字列型、ageプロパティが数値型である型を定義しています。

type PersonType = { name: string; age: number }
interface PersonInterface { name: string; age: number }

TypeScriptでは、型エイリアスは継承をサポートしていません。つまり、extendsキーワードを使って、他の型を継承する新しい型を作ることはできません。しかし、&演算子を使えば似たような動作をさせることができます。

演算子を使用すると、2つ以上の型を組み合わせて新しい型を作成することができます。これは、複数の既存の型のプロパティとメソッドをすべて含む新しい型を作成するために使用できます。

type Person = { name: string; age: number };
type Employee = { salary: number; work(): void };
type Worker = Person & Employee;

この例では、Worker型はPerson型とEmployee型の論理和なので、name、age、salaryの各プロパティとwork()メソッドを備えています。

基本的には、一部のプロパティやメソッドを追加またはオーバーライドすることはできません。すべての型のすべてのプロパティとメソッドを結合するだけになります。ただ、ユーティリティ型のOmitを使用することで必要なプロパティのみをオーバーライドすることも可能です。

例えば次の例においてworkプロパティはオーバーライドされません。

type Worker = Person & Omit<Employee, "work">;

また、インターフェースを使用すると、上の例と同じ動作を実現できますが、インターフェースは多重継承が可能であり、どのプロパティやメソッドがどのインターフェースから来たものかを理解しやすいため、より表現力や保守性が高くなります。

TypeScript インターフェースの多重継承

TypeScriptでは、インターフェースは多重継承をサポートしており、一つのインターフェースが他の複数のインターフェースを拡張することができます。これにより、他の複数のインターフェースのプロパティやメソッドを組み合わせたインターフェースを作成することができます。

interface Animal {
    name: string;
}
interface Mammal {
    hasFur: boolean;
}
interface Fish {
    swims: boolean;
}
interface Dolphin extends Animal, Mammal, Fish {
    swimsFast: boolean;
}

この例では、動物の異なる性質を記述する3つのインターフェース Animal, Mammal, Fishを用意しました。そして、3つのインターフェースを拡張した1つのインターフェースDolphinがあります。つまり、Dolphinオブジェクトはname、hasFur、swimsというプロパティを持ち、さらにswimsFastという独自のプロパティも持っています。

これにより、より具体的で表現力のあるインターフェースを作ることができ、また、複数のインターフェースを実装したクラスをより柔軟に使用することができます。

また、多重継承は便利な場合もありますが、特に大規模なコードベースや複雑なクラス階層を扱う場合には、複雑で混乱を招く可能性があります。使用する際には、トレードオフを考慮し、賢明に使用することが重要です。

Note: オブジェクト指向プログラミングについては、CS基礎/プログラミングパラダイム/OOPで詳しく学習できます。

この記事を書いた人

著者の画像

Jeffry Alvarado

Ex-Facebook Engineer 大学ではコンピュータサイエンスを専攻し、在学中に複数のインターンシップを経験。コンピュータサイエンスが学習できるプラットフォームRecursionを創業し、CTOとしてカリキュラム作成、ソフトウェア開発を担当。


ツイート