TypeScriptのinterface
は、オブジェクトの構造を定義するための機能です。簡単に言うと、interface
は特定のオブジェクトが持つべきプロパティやメソッドの「設計図」です。interface
を適切に利用することで、コード内で一貫性と予測可能性を保ちながら、エラーを減らし開発プロセスをスムーズにすることができます。
interface User { name: string; age: number; isAdmin: boolean; } function registerUser(user: User) { // userオブジェクトがUserインターフェースに従っていることが保証されています。 }
ポイント interface
の主なポイント
- 型の定義: オブジェクトが特定の構造を持つことを保証。
- コードの再利用性向上: 同じインターフェースを様々な場所で再利用可能。
- 開発プロセスの支援: エディタの自動補完機能により、プロパティやメソッドの入力を容易に。
- 保守性の向上: コードベースが成長しても、型の安全性を維持しやすくなります。
interface
を使うことで、TypeScriptの型システムの全能力を活用し、より安全で読みやすく、保守しやすいコードを書くことができます。このページでは、TypeScriptのキーファクターとなるinterface
の基本~使い方、利用する際の注意点やTipsをわかりやすくご説明します。
Webエンジニア/Webデザイナーを目指す方であれば知らないと恥ずかしい超・基本知識の1つです。是非最後までご覧ください。
参考 【JavaScript入門】基本文法/基礎文法を5分で
TypeScript:interaceとは?
interface
(インターフェース)は、TypeScriptにおけるオブジェクトの形状(構造)を定義するための強力なツールです。特定のオブジェクトが持つべきプロパティやメソッドの型を定義することで、コードの安全性と予測可能性を高めるために用いられます。
インターフェースは建物を建てる際の「設計図」のようなものだと言えます。設計図(インターフェース)は、建築物(オブジェクト)の構造を定義し、部屋の配置やサイズ、材料の種類や建物の耐久性など建築物が満たすべき具体的な要件(プロパティ)を示す役割を担います。
設計図(インターフェース)の主な目的は、家がどのように見えどう機能するかを示すことです。 建築家(プログラマー)は、この設計図に基づいて家(オブジェクト)を構想しますが、実際に家を建てるのは建設業者(クラス)です。設計図は具体的な建築材料や建築方法を指定しませんが、最終的な建物が満たすべき基本的な要件や構造を定義します。
もし途中で窓をもう1つ追加することになった場合、設計図(インターフェース)にこの変更を加えるだけで、建設プロジェクト全体がこの新しい要件を反映するようになります。
インターフェースの概要はここまで。実際のコードを見ていった方がより具体的にイメージが付きやすいと思いますので、ここからは、順を追ってインターフェースの定義方法~利用方法をご説明していきます。
ステップ1:インターフェースの定義
インターフェースの定義方法は以下の通り。
interface インターフェース名 { プロパティ名: 型; // 他のプロパティやメソッドの定義 }
例えば、次のようにPerson
インターフェースを定義することができます。↓のインターフェースは、name
プロパティが文字列型で、age
プロパティが数値型であることを示しています。
interface Person { name: string; age: number; }
インターフェースは他のインターフェースをネストすることも可能です。ネストすることで、プロパティがさらにオブジェクトであるような場合に、そのオブジェクトの形状を明確に指定することができるようになります。
interface Address { street: string; city: string; zipCode: number; } interface Person { name: string; age: number; address: Address; // Addressインターフェースを使用 } const person: Person = { name: "Taro Yamada", age: 30, address: { street: "123 Maple St", city: "Tokyo", zipCode: 1000001 } };
↑のサンプルコードでは、Person
インターフェースがAddress
インターフェースをネストして使用しています。これにより、person
オブジェクトに含まれるaddress
プロパティが、Address
インターフェースで定義された形状に従っていることが保証されます。
ネストされたインターフェースを使用することで、実際のアプリケーションのデータ構造をより正確にモデル化できます。例えば、企業や製品のカタログ、ユーザーインターフェースのコンポーネントの設定など、階層的なデータ構造を持つ多くのシナリオで役立ちます。
interface Employee { name: string; position: string; } interface Company { name: string; employees: Employee[]; // Employeeインターフェースの配列 } const myCompany: Company = { name: "Tech Solutions", employees: [ { name: "John Doe", position: "Software Developer" }, { name: "Jane Smith", position: "Project Manager" } ] };
ステップ2:インターフェースを使用してオブジェクト型を定義する
インターフェースを定義したら、それを使ってオブジェクトの型を指定することができます。以下のように、オブジェクトリテラル({})でオブジェクトを作成する際に、その型としてインターフェースを指定します。
let john: Person = { name: "John Doe", age: 32 };
ここで、john
オブジェクトはPerson
インターフェースに従っているため、name
とage
プロパティを持っている必要があります。もしこれを満たさない場合、TypeScriptがコンパイル時にエラーを発生させます。
このように「型」の不一致をエラーにすることで、予期せぬバグやオブジェクト間での不整合を防止することができます。
ステップ3:任意プロパティの定義
インターフェースでは任意プロパティも定義することが可能です。これにより、あるプロパティがオブジェクトに存在しなくても良い場合に柔軟に対応することができるようになります。
任意プロパティはプロパティ名の後に?
をつけて定義します。
interface Profile { name: string; age?: number; // 任意プロパティ }
このProfile
インターフェースでは、name
プロパティは必須ですが、age
プロパティは任意→つまり、age
プロパティを含むProfile
オブジェクトも、含まないProfile
オブジェクトも、どちらもProfile
インターフェースの要件を満たすということです。
サンプルコード 任意プロパティの使用例
let person1: Profile = { name: "Alice" }; let person2: Profile = { name: "Bob", age: 30 };
ポイント 任意プロパティの利点
- 柔軟性: データモデルがすべてのプロパティを必須としない場合に柔軟に対応可能。
- 進化するAPI: APIからのレスポンスなど、時間と共に変化する可能性のあるオブジェクト構造を表現する際に便利。
- オプションの設定: ユーザーによる設定オプションなど、存在しない場合にデフォルト値を使うプロパティの表現に適しています。
任意プロパティを使う場合、プロパティが存在しない可能性を考慮したコードの記述が必要になります。例えば、undefined
チェックを行うなどして、プログラムが安全に動作するようにします。
if (person2.age !== undefined) { console.log(person2.age); // ageが存在する場合のみログを出力 }
以上のように、任意プロパティはTypeScriptのインターフェースを使った型定義の柔軟性を高める重要な機能です。
ステップ4:インターフェースの継承
インターフェースは他のインターフェースを継承し、新しいインターフェースを作成することができます。インターフェースを継承することで、既存のインターフェースのプロパティを新しいインターフェースに「引き継ぐ」ことができ、これにより複数のインターフェースの特性を組み合わせて、新しい複合型を形成することができます。
インターフェースの継承はextends
キーワードを使用して行います。
interface NamedEntity { name: string; } interface Person extends NamedEntity { age: number; }
↑の例では、Person
インターフェースはNamedEntity
インターフェースからname
プロパティを継承します。その結果、Person
インターフェースはname
プロパティとage
プロパティの両方を持つことになります。
また、TypeScriptではインターフェースは複数のインターフェースを継承することができ、異なる特性を持つ複数のインターフェースを1つにまとめることができます。
interface NamedEntity { name: string; } interface Ageable { age: number; } interface Employee extends NamedEntity, Ageable { employeeId: number; }
Employee
インターフェースはNamedEntity
とAgeable
の両方からプロパティを継承するので、Employee
インターフェースはname
、age
、そしてemployeeId
プロパティを持つことになります。
ただし、インターフェースの継承を使用する際は、継承の階層が複雑になりすぎないように注意する必要があります。
階層が深くなると、コードの理解やデバッグが難しくなる可能性があります。また、不必要に多くのプロパティやメソッドを含むインターフェースが形成されることを避けるため、継承を使用する際は慎重に設計することが重要です。
ステップ5:インターフェースを実装するクラス
クラスがインターフェースを実装するとき、そのクラスはインターフェースに定義されているすべてのプロパティとメソッドを持つ必要があります。これは、implements
キーワードを使って行います。
interface Greetable { greet(): void; } class Person implements Greetable { name: string; constructor(name: string) { this.name = name; } greet() { console.log(`Hello, my name is ${this.name}`); } }
Person
クラスはGreetable
インターフェースを実装しています。これにより、Person
クラスはgreet
メソッドを実装する必要があり、この契約を満たすために、greet
メソッドがクラス内に定義されています。
なお、TypeScriptではクラスは複数のインターフェースを実装することができます。異なる機能のセットを組み合わせて、より複雑な振る舞いを持つクラスを作成できます。
interface Greetable { greet(): void; } interface Identifiable { id: number; } class Employee implements Greetable, Identifiable { name: string; id: number; constructor(name: string, id: number) { this.name = name; this.id = id; } greet() { console.log(`Hello, my name is ${this.name} and my ID is ${this.id}`); } }
ポイント インターフェースの実装の利点
- 型安全: インターフェースを実装するクラスは、インターフェースの契約に従う必要があるため、コードの型安全性が向上。
- 再利用性と保守性: 共通のインターフェースを実装する複数のクラス間でコードを再利用しやすくなる。また、インターフェースを変更すると、それに依存するすべてのクラスに変更が自動的に反映されるため、保守性が向上する。
- 柔軟性: インターフェースを利用することで、実装の詳細を抽象化し、クラス間の依存性を減らすことができる。
インターフェースとクラスの関係性を理解し、適切に活用することで、より強力で柔軟なコードを書くことができます。
インターフェースの基本を押さえたところで、ここからはより実践的なテクニックやTipsをご紹介します。
インデックス署名
インデックス署名は、TypeScriptにおいてオブジェクトのプロパティ名とその型があらかじめ特定できない場合に使える便利な機能です。いんですが署名は動的なプロパティ名に対応し、さまざまなキーとその値の型を柔軟に定義できます。
インデックス署名は以下の形式で定義されます。
interface インターフェース名 { [key: キータイプ]: バリュータイプ; }
サンプルコード 文字列のキーを持つ場合
interface StringDictionary { [key: string]: number; } const myDict: StringDictionary = { apple: 5, orange: 10, banana: 20 };
↑の例では、StringDictionary
インターフェースは文字列のキーと数値の値を持つオブジェクト([key: string]: number)を表します。この定義により、任意の文字列のキーに対して数値を割り当てることができるという仕組み。
サンプルコード 数値のキーを持つ場合
interface NumberDictionary { [index: number]: string; } const myArray: NumberDictionary = { 10: "ten", 20: "twenty", 30: "thirty" };
インデックス署名を使用することで、オブジェクトのプロパティ名を動的に定義できるため、APIからのレスポンスなど予測不可能なプロパティ名を持つデータ構造を扱いやすくなります。また、インデックス署名により、キーと値の型を厳密に指定できるため、型安全性が向上します。
インデックス署名を使用する際には、キーと値の型を正しく設定することが重要です。また、インデックス署名で定義された型以外のキーまたは値を割り当てようとすると、TypeScriptコンパイラはエラーを報告します。
関数型のインターフェース
TypeScriptでは、関数の型を定義する場合にもインターフェースを使用することができます。→関数が受け取る引数の型や返す値の型を厳密に指定することができ、プログラムの型安全性を向上させることができます。
関数のインターフェースを定義するには、インターフェース内に呼び出しシグネチャを記述します。呼び出しシグネチャは、関数の引数のリストと戻り値の型を定義します。
interface 関数名 { (引数1: 型1, 引数2: 型2, ...): 戻り値の型; }
サンプルコード 単純な関数インターフェース
interface GreetFunction { (name: string): string; } const greet: GreetFunction = function(name: string) { return `Hello, ${name}!`; }; console.log(greet("Alice")); // "Hello, Alice!"
GreetFunction
インターフェースは、string
型の引数を1つ取り、string
型の値を返す関数の型を定義しています。
サンプルコード 複数の引数を取る関数インターフェース
interface CalculateSumFunction { (x: number, y: number): number; } const sum: CalculateSumFunction = function(x: number, y: number) { return x + y; }; console.log(sum(1, 2)); // 3
関数のインターフェースを使用することで、関数が期待する引数の型と戻り値の型を厳密に指定することが可能になります。結果、間違った型のデータを関数に渡すことを防ぎ、プログラムの安全性を向上させることに繋がります。また、関数インターフェースは関数がどのような引数を取り、どのような値を返すかを明確に示します。これにより、コードの可読性が向上し、他の開発者がコードを理解しやすくなるという利点があります。。
インターフェース宣言のマージ(Declaration Merging)
TypeScriptでは「宣言のマージ(Declaration Merging)」という強力な機能があります。これは、同じ名前の2つ以上の宣言を1つの宣言として合体させることができるという仕組みのこと。分散していたインターフェースの定義を統合し拡張することができます。
interface Animal { name: string; } interface Animal { age: number; } // マージされたインターフェース // interface Animal { // name: string; // age: number; // }
Animal
インターフェースが2つ定義されていますが、TypeScriptはこれらをマージして1つのインターフェースとして扱います。その結果、マージされたAnimal
インターフェースはname
プロパティとage
プロパティの両方を持つことになります。
これらの機能と概念を理解することで、TypeScriptのインターフェースをより効果的に利用し、より安全でメンテナンスしやすいコードを書くことができるようになります。網羅的に学習することは大変価値があり、TypeScriptの強力な型システムの利点を最大限に活用することができます。