interfaceとtypeの違い
型エイリアスを利用することで、インターフェースと同様の定義が行なえます。
tsinterfaceAnimal {name : string;bark (): string;}typeAnimal = {name : string;bark (): string;};
tsinterfaceAnimal {name : string;bark (): string;}typeAnimal = {name : string;bark (): string;};
この章では、インターフェースと型エイリアスの違いについて詳しく説明していきます。
インターフェースと型エイリアスの違い
| 内容 | インターフェース | 型エイリアス |
|---|---|---|
| 継承 | 可能 | 不可。ただし交差型で表現は可能 |
| 継承による上書き | 上書きまたはエラー | フィールド毎に交差型が計算される |
| 同名のものを宣言 | 定義がマージされる | エラー |
| Mapped Types | 使用不可 | 使用可能 |
| インデックス型への代入互換性 | なし | あり |
継承
インターフェースは、インターフェースや型エイリアスを継承できます。
tsinterfaceAnimal {name : string;}typeCreature = {dna : string;};interfaceDog extendsAnimal ,Creature {dogType : string;}
tsinterfaceAnimal {name : string;}typeCreature = {dna : string;};interfaceDog extendsAnimal ,Creature {dogType : string;}
一方、型エイリアスは継承は行えません。代わりに交差型(&)を使用することで、継承と似たことを実現できます。
tstypeAnimal = {name : string;};typeCreature = {dna : string;};typeDog =Animal &Creature & {dogType : string;};
tstypeAnimal = {name : string;};typeCreature = {dna : string;};typeDog =Animal &Creature & {dogType : string;};
プロパティのオーバーライド
インターフェースで継承の際にプロパティをオーバーライドすると、継承元のプロパティの型が上書きされます。
ts// OKinterfaceAnimal {name : any;price : {yen : number;};legCount : number;}interfaceDog extendsAnimal {name : string;price : {yen : number;dollar : number;};}// 最終的なDogの定義interfaceDog {name : string;price : {yen : number;dollar : number;};legCount : number;}
ts// OKinterfaceAnimal {name : any;price : {yen : number;};legCount : number;}interfaceDog extendsAnimal {name : string;price : {yen : number;dollar : number;};}// 最終的なDogの定義interfaceDog {name : string;price : {yen : number;dollar : number;};legCount : number;}
ただし、オーバーライドするためには元の型に代入できるものでなければなりません。次の例はnumber型であるフィールドをstring型でオーバーライドしようとしている例です。
tsinterfaceA {numberField : number;price : {yen : number;dollar : number;};}interfaceInterface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.2430Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.extends B A {numberField : string;price : {yen : number;euro : number;};}
tsinterfaceA {numberField : number;price : {yen : number;dollar : number;};}interfaceInterface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.2430Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.extends B A {numberField : string;price : {yen : number;euro : number;};}
一方、型エイリアスの場合は上書きにはならず、フィールドの型の交差型が計算されます。また、交差型で矛盾があって計算できない場合もコンパイルエラーにはなりません。
tstypeAnimal = {name : number;price : {yen : number;dollar : number;};};typeDog =Animal & {name : string;price : {yen : number;euro : number;};};// 最終的なDogの定義typeDog = {name : never; // 交差型を作れない場合はコンパイルエラーではなくnever型になるprice : {yen : number;dollar : number;euro : number;};};
tstypeAnimal = {name : number;price : {yen : number;dollar : number;};};typeDog =Animal & {name : string;price : {yen : number;euro : number;};};// 最終的なDogの定義typeDog = {name : never; // 交差型を作れない場合はコンパイルエラーではなくnever型になるprice : {yen : number;dollar : number;euro : number;};};
同名のものを宣言
型エイリアスは同名のものを複数定義できず、コンパイルエラーになります。
tstypeDuplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.= { SameNameTypeWillError message : string;};typeDuplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.= { SameNameTypeWillError detail : string;};
tstypeDuplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.= { SameNameTypeWillError message : string;};typeDuplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.= { SameNameTypeWillError detail : string;};
一方、インターフェースの場合は、同名のインターフェースを定義でき、同名の定義をすべて合成したインターフェースになります。
ただし、同名のフィールドだが、型の定義が違っている場合はコンパイルエラーになります。
tsinterfaceSameNameInterfaceIsAllowed {myField : string;sameNameSameTypeIsAllowed : number;sameNameDifferentTypeIsNotAllowed : string;}interfaceSameNameInterfaceIsAllowed {newField : string;sameNameSameTypeIsAllowed : number;}interfaceSameNameInterfaceIsAllowed {Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.2717Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.: number; sameNameDifferentTypeIsNotAllowed }
tsinterfaceSameNameInterfaceIsAllowed {myField : string;sameNameSameTypeIsAllowed : number;sameNameDifferentTypeIsNotAllowed : string;}interfaceSameNameInterfaceIsAllowed {newField : string;sameNameSameTypeIsAllowed : number;}interfaceSameNameInterfaceIsAllowed {Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.2717Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.: number; sameNameDifferentTypeIsNotAllowed }
Mapped Types
Mapped Typesについては別のページで詳しく説明しますので、ここでは型エイリアスとインターフェースのどちらで使えるかだけを説明します。
📄️ Mapped Types
インデックス型では設定時はどのようなキーも自由に設定できてしまい、アクセス時は毎回undefinedかどうかの型チェックが必要です。入力の形式が決まっているのであればMapped Typesの使用を検討できます。
Mapped Typesは型のキーを動的に指定することができる仕組みであり、型エイリアスでのみ利用することができます。
次の例ではユニオン型の一覧をキーとした新しい型を生成しています。
typescripttypeSystemSupportLanguage = "en" | "fr" | "it" | "es";typeButterfly = {[key inSystemSupportLanguage ]: string;};
typescripttypeSystemSupportLanguage = "en" | "fr" | "it" | "es";typeButterfly = {[key inSystemSupportLanguage ]: string;};
インターフェースでMapped Typesを使うとエラーになります。
typescripttypeSystemSupportLanguage = "en" | "fr" | "it" | "es";interfaceButterfly {[A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.key inSystemSupportLanguage ]: string;}
typescripttypeSystemSupportLanguage = "en" | "fr" | "it" | "es";interfaceButterfly {[A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.key inSystemSupportLanguage ]: string;}
インデックス型への代入互換性
インターフェースと型エイリアスでは、インデックス型への代入互換性が異なります。
- 型エイリアス: インデックス型に代入できる
- インターフェース: インデックス型に代入できない
次の例では、同じ構造を持つインターフェースと型エイリアスを、インデックス型に代入しています。
tsinterfacePersonInterface {name : string;age : string;}typePersonType = {name : string;age : string;};constpersonInterface :PersonInterface = {name : "Alice",age : "25" };constpersonType :PersonType = {name : "Bob",age : "30" };// インターフェースはインデックス型に代入できないconstType 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.2322Type 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.: { [ stringMap1 K : string]: string } =personInterface ;// 型エイリアスはインデックス型に代入できるconststringMap2 : { [K : string]: string } =personType ; // OK
tsinterfacePersonInterface {name : string;age : string;}typePersonType = {name : string;age : string;};constpersonInterface :PersonInterface = {name : "Alice",age : "25" };constpersonType :PersonType = {name : "Bob",age : "30" };// インターフェースはインデックス型に代入できないconstType 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.2322Type 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.: { [ stringMap1 K : string]: string } =personInterface ;// 型エイリアスはインデックス型に代入できるconststringMap2 : { [K : string]: string } =personType ; // OK
実際のユースケース
この違いは、Record<string, T>のようなユーティリティ型を使う場面でよく遭遇します。
tsinterfaceUser {name : string;}functionprintUser (user :Record <string, string>) {console .log (user );}constuser :User = {name : "alice",Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.2345Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.printUser (); // エラー user
tsinterfaceUser {name : string;}functionprintUser (user :Record <string, string>) {console .log (user );}constuser :User = {name : "alice",Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.2345Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.printUser (); // エラー user
このような場合、型エイリアスで定義するか、後述の回避策を使う必要があります。
回避策
インターフェースをインデックス型に代入したい場合、いくつかの回避策があります。
値レベルの回避策: スプレッド構文を使う
スプレッド構文を使うと、インターフェースの型情報が再計算され、代入できるようになります。
tsconststringMap : { [K : string]: string } = { ...personInterface }; // OK
tsconststringMap : { [K : string]: string } = { ...personInterface }; // OK
型レベルの回避策: Pickを使う
Pickを使うと、インターフェースから型エイリアス相当の型を作成できます。
tstypePersonType =Pick <PersonInterface , keyofPersonInterface >;typeTest =OnlyStringValues <PersonType >; // OK
tstypePersonType =Pick <PersonInterface , keyofPersonInterface >;typeTest =OnlyStringValues <PersonType >; // OK
インターフェースにインデックス型を明示的に追加する
インターフェースの定義を変更できる場合は、インデックス型を明示的に追加する方法もあります。
tsinterfacePersonInterface {name : string;age : string;[K : string]: string; // 明示的にインデックス型を追加}constperson :PersonInterface = {name : "Alice",age : "25" };conststringMap : { [K : string]: string } =person ; // OK
tsinterfacePersonInterface {name : string;age : string;[K : string]: string; // 明示的にインデックス型を追加}constperson :PersonInterface = {name : "Alice",age : "25" };conststringMap : { [K : string]: string } =person ; // OK
📄️ インデックス型
TypeScriptで、オブジェクトのフィールド名をあえて指定せず、プロパティのみを指定したい場合があります。そのときに使えるのがこのインデックス型(index signature)です。たとえば、プロパティがすべてnumber型であるオブジェクトは次のように型注釈します。
インターフェースと型エイリアスの使い分け
実際に型を定義する時にインターフェースと型エイリアスのどちらを使うのがよいのでしょうか?残念ながら、これに関しては明確な正解はありません。
インターフェースと型エイリアスのどちらでも型を定義することができますが、拡張性やMapped Typesの利用可否といった点で異なる部分が存在するので、これらのメリット・デメリットを考慮してプロジェクト内でルールを決めてそれに遵守するようにしましょう。
参考例として、Googleが公開しているTypeScriptのスタイルガイドの型エイリアスvsインターフェースの項目では、プリミティブな値やユニオン型やタプルの型定義をする場合は型エイリアスを利用し、オブジェクトの型を定義する場合はインターフェースを使うことを推奨しています。
インターフェースと型エイリアスの使い分けに悩む場面が多く開発スピードが落ちてしまうのであれば、型エイリアスに統一して書く方針にする考え方もあります。
インターフェースの利用例
ライブラリを作成する際に定義した型の構造がアプリケーション側に依存するような場合にはインターフェースを利用するのが適切です。
Node.jsのprocess.envの型定義は@types/node/process.d.tsで次のように実装されています。
tsdeclare module "process" {global {namespaceNodeJS {interfaceProcessEnv extendsDict <string> {TZ ?: string;}}}}
tsdeclare module "process" {global {namespaceNodeJS {interfaceProcessEnv extendsDict <string> {TZ ?: string;}}}}
インターフェースで型定義されていることで、パッケージを利用する側で型の拡張が自由に行えるようになっています。
もしProcessEnvが型エイリアスで定義されていると型の拡張が行えず、とても開発しづらい状態になってしまいます。このように不特定多数のユーザーが型を参照するような場合には、拡張性を考慮してインターフェースで型定義をするようにしましょう。
ts// src/types/global.d.tsdeclare module "process" {global {namespaceNodeJS {interfaceProcessEnv {NODE_ENV : "development" | "production";}}}}
ts// src/types/global.d.tsdeclare module "process" {global {namespaceNodeJS {interfaceProcessEnv {NODE_ENV : "development" | "production";}}}}
関連情報
📄️ インターフェース
インターフェースはクラスが実装すべきフィールドやメソッドを定義した型です。クラスはインターフェースを実装することで、インターフェースが求めるメソッド名や引数の型に則っているかをチェックすることができます。
📄️ 型エイリアス
TypeScriptでは、型に名前をつけられます。名前のついた型を型エイリアス(タイプエイリアス; type alias)と呼びます。