【TypeScript】インターフェイスの定義と使用方法、型エイリアスとの違い

TypeScript

TypeScriptのインターフェイスは、開発者自身が定義できるデータ型(ユーザー定義型)のひとつで、主にオブジェクトの構造を定義する強力なツールです。
オブジェクトの形状を記述し、型チェックを行うことで、より安全で保守性の高いコードを書くことができます。

本記事では、インターフェイスの基本的な定義方法と使用法について実例を交えて解説するとともに、似通った機能を持つ「型エイリアス」との違いについても言及します。

インターフェイスの基本

インターフェイスは interface キーワードを使用して定義します。
オブジェクトの構造として、プロパティ関数(メソッド)を持つことができます。

以下は、プロパティのみを持つシンプルなインターフェイスの例です。

TypeScript
interface Person {
  name: string; // 文字列型のプロパティ name
  age: number; // 数値型のプロパティ age
}

上記の Person インターフェイスを使用してみましょう。

TypeScript
function greet(person: Person): string {
  return `私は${person.name}です。${person.age}歳です。`;
}

let user: Person = { name: "Rey", age: 25 };
console.log( greet(user) );

実際にプログラミングして、実行結果を確認してみてください。

まだ環境構築を実施されていない、コンパイルと実行方法がわからないという方は、こちらの記事を参照ください。

環境構築は面倒だが少し試してみたいだけ、という方は、Webブラウザ上でコーディングから実行まで可能な TypeScript: プレイグラウンド でお試しください。

オプショナルプロパティ

インターフェイスに、必須ではないプロパティを定義したい場合もあります。
そのような場合、プロパティ名の後に 「?」 を付けることでオプショナルプロパティとして定義できます。

TypeScript
interface Car {
  make: string;
  model: string;
  year?: number; // 👈省略可能なオプショナルプロパティ
}

let myCar: Car = { make: "Toyota", model: "Crown" };

読み取り専用プロパティ

インターフェイスに変更不可のプロパティを定義したい場合、プロパティ名の前に readonly キーワードを使用します。

TypeScript
interface Point {
  readonly x: number;
  readonly y: number;
}

let p1: Point = { x: 10, y: 20 }; // 初期化はOK
p1.x = 5; // 👈エラーになる:読み取り専用プロパティは変更できません。

メソッドの定義

インターフェースにはメソッドも定義できます。
と言っても、具体的な実装(処理の中身)を記述することはできず、メソッドの形式を定義しておくだけになります。

JavaやC#、Kotlinのインターフェイスには、デフォルトメソッドと呼ばれる実装ありのメソッドを定義できます。

上記の言語経験者は、TypeScriptのインターフェイスとの違いにご注意ください。

では、メソッドの実装(処理の中身)をどこに記述するのか、という疑問が浮かびます。
それは、インターフェイスを実装するクラスが担当します。

メソッドを定義したインターフェイスと実装の例

インターフェイスを実装するのはクラス(class)です。
implements というキーワードを使用して、実装するインターフェイスを指定します。

TypeScript
// 👇インターフェイス Greeter は、メソッド greet を定義しています。
interface Greeter {
  greet(name: string): string;
}

// 👇クラス EnglishGreeter は、Greeterインターフェイスを実装しています。
class EnglishGreeter implements Greeter {
  greet(name: string) {
    return `Hello, ${name}!`;
  }
}

上記のプログラムを動作させてみましょう。

次のコードを追加し、コンパイルした後、実行してみてください。

TypeScript
const englishGreeter = new EnglishGreeter();
console.log( englishGreeter.greet("Leia") );
// 👆 "Hello, Leia!" とコンソールに表示されます。

インターフェイスの拡張(継承)

インターフェースは、extends キーワードを使用して他のインターフェースを拡張(継承)することができます。この機能はコードの再利用性と可読性を向上させます。

TypeScript
interface Animal {
  name: string;
}

// Cat は Animal のプロパティを継承する
interface Cat extends Animal {
  breed: string;
}

let myCat: Cat = { name: "タマ", breed: "マンチカン" };

インデックスシグネチャ

TypeScriptのインデックスシグネチャは、オブジェクトのプロパティが事前に決定されていない場合に使用されます。

インデックスシグネチャを使用すると、特定の型のキーと値を持つオブジェクトを定義できます。
これにより、動的にプロパティの追加、アクセスする際に型安全性を保つことができます。

シンプルなインデックスシグネチャの例

TypeScript
// 👇キーが数値型、値が文字列型のインデックスシグネチャ
interface StringArray {
  [index: number]: string;
}

// 👇配列なので、インデックスが0から順に振られる
let myArray: StringArray = ["Luke", "Han"];

let myStr1: string = myArray[0]; // "Luke"
let myStr2: string = myArray[1]; // "Han"

インデックスシグネチャが使用されるケース

  1. 動的なプロパティ: プロパティの名前や数が動的に決定される場合に使用されます。
    • 例: ユーザーから入力されたデータをキーと値のペアとして保存する場合。
  2. 辞書オブジェクト: 辞書やマップのように、キーと値のペアを持つオブジェクトを定義する場合に使用されます。
    • 例: 国コードと国名の対応表を持つオブジェクト。
  3. 柔軟なオブジェクト構造: あらかじめ定義されていないプロパティを持つオブジェクトを表現する場合に使用されます。
    • 例: APIレスポンスのデータを格納するためのオブジェクト。

複数の型を持つインデックスシグネチャ

複数の型を持つ場合でも、ユニオンを使用するといった手法によりインデックスシグネチャを使用できますが、基本的には単一(一種類)の型に対して定義されることが一般的です。

関数型インターフェイス

TypeScriptの関数型インターフェイスは、関数のシグネチャを型として定義する場合に使用されます。これにより、関数の引数の名前や型、戻り値の型を明確に定義し、型安全性を確保しつつ再利用可能な関数シグネチャを作成することができます。

関数型インターフェイスは以下の書式で定義します。

引数なしの場合
(): 戻り値の型
引数ありの場合
( 引数1: 型[, 引数2: 型 ...] ): 戻り値の型

※ [ ] 内は引数が複数あった場合

関数型インターフェイスが使用されるケース

  1. 関数のシグネチャを定義する場合:関数型インターフェイスを使用して、関数のシグネチャを定義することで、関数の型を統一できます。
  2. コールバック関数を型安全に扱う場合:関数型インターフェイスを使用すると、コールバック関数の型を明確に定義できるため、型安全なコールバック処理が可能になります。
  3. 関数の型を再利用する場合:関数型インターフェイスを使用することで、同じシグネチャを持つ複数の関数に対して、型を再利用できます。

それぞれの具体例を示します。

1. 関数のシグネチャを定義する場合の例

TypeScript
interface SearchFunc {
  (source: string, word: string): boolean;
}

const mySearch: SearchFunc = (source, word) => {
  return source.includes(word);
};

console.log(mySearch("TypeScript", "Script")); // true と表示されます

上記の例では、SearchFunc インターフェイスが関数のシグネチャを定義しています。mySearch 関数は SearchFunc 型を持ち、そのシグネチャに従って実装されます。

2. コールバック関数を型安全に扱う場合

TypeScript
interface Callback {
  (error: Error | null, result?: string): void;
}

function fetchData(callback: Callback) {
  setTimeout(() => {
    callback(null, "Data fetched successfully");
  }, 1000);
}

fetchData((error, result) => {
  if (error) {
    console.error(error);
  } else {
    console.log(result); // "Data fetched successfully" と表示されます
  }
});

上記の例では、Callback インターフェイスがコールバック関数のシグネチャを定義しています。fetchData 関数は、コールバック関数を引数として受け取り、そのシグネチャに従ってコールバックを実行します。

3. 関数の型を再利用する場合

TypeScript
interface MathOperation {
  (a: number, b: number): number;
}

const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;

console.log( add(2, 3) ); // 5 と表示されます
console.log( multiply(2, 3) ); // 6 と表示されます

上記の例では、MathOperation インターフェイスが関数のシグネチャを定義しており、add 関数と multiply 関数がともに MathOperation 型であるため、同一のシグネチャに従って実装されています。

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

TypeScriptには、インターフェイスと似通った機能を持つ「型エイリアス」(type)があります。

共通点

  • 両者ともオブジェクト型や関数型などを定義できる。
  • プロパティやメソッドを含む型を記述できる。
  • 型アノテーション(型注釈)として使用できる。

比較マトリックス

機能/特徴インターフェイス型エイリアス
クラスの実装を強制×
マージ(再オープン)(プロパティ・メソッドを追加可能)×
ユニオン型×
タプル型×
オブジェクトの構造定義
プリミティヴ型定義△(基本はオブジェクト用)
交差型(インターセクション型)△(拡張インターフェイスとして)
関数型△(メソッドとして可能)
複雑な型の表現△(拡張インターフェイスとして)
  • :一方のみできる
  • 〇:できる
  • △:限定的にできる
  • ×:できない

使い分けのポイント

  • インターフェイス
    • オブジェクトの構造を定義する場合や、クラスに実装を強制する場合に適しています。特に、プロパティの追加や拡張が必要な場合に便利です。
    • ユニオン型を使用できない点に注意が必要です。
  • 型エイリアス
    • より柔軟な型定義や、ユニオン型、交差型、関数型などの表現が必要な場合に適しています。
    • マージ(再オープン)できないため、一度定義された型に追加のプロパティを加えることはできません。

まとめ

本記事では基本的な定義方法から高度な使用方法まで解説するとともに、似通った機能である「型エイリアス」との違いについても触れました。

両者には共通点も多いですが、それぞれ得意とする場面が異なるため、適切に使い分けることが重要です。
次回の記事では「型エイリアス」についてさらに詳しく掘り下げ、その応用例をご紹介します。


本記事についての質問、誤りの指摘、ご意見ご感想などありましたら、ぜひコメント頂ければ幸いです。

最後までお読みいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました