TypeScriptの interface
と type
の違い
TypeScriptを採用する最大の利点は、強力な型システムにあります。
型の名前を定義する type
と interface
の2つのオプションには、類似点が多いです。
プロジェクトでどちらかを使う上で迷いが出てしまうことがあるので、これらのオプションについて特性を調べました。
TypeScript v3.7.2 の Playground で検証しています。
TL;DR
-
type
とinterface
の類似点と違いを理解します。 - いずれかの構文を使用して同じ型を記述する方法を知る。
- プロジェクトでどちらを使用するかを決める際には、確立されたスタイルと拡張性が有用かどうかを検討する必要がある。
類似点と違い
TypeScriptで型の名前を定義する。
type TState = { name : string ; capital : string ; }
interface IState { name : string ; capital : string ; }
普段どちらを選択しているでしょうか?
これら2つのオプションの境界線は、長年にわたってますます曖昧になってきています。多くの場面で両方とも利用できます。
型は互いにほとんど区別できません。
未定義プロパティを使用したときに発生するエラー内容は、 IState
、 TState
で同様です。
const wyoming : TState = {
name : 'Wyoming' ,
capital : 'Cheyenne' ,
population : 500000 // ~~~~~~~~~~~~~~~~~~ Type ... is not assignable to type 'TState' // Object literal may only specify known properties, and // 'population' does not exist in type 'TState'
};
type
と interface
でどちらもkeyの型も指定できます。
type TDict = { [ key : string ] : string };
interface IDict { [ key : string ] : string ; }
関数の型も定義できます。
type TFn = ( x : number ) => string ;
interface IFn { ( x : number ) : string ; }
const toStrT : TFn = x => '' + x ; // OK
const toStrI : IFn = x => '' + x ; // OK
interface
は型を拡張できます。
また、type
は interface
を拡張できます。
interface IStateWithPop extends TState {
population : number ;
}
type TStateWithPop = IState & { population : number ; };
繰り返しますが、これらの型は同じです。
注意点は、interface
はunion型のような複雑な型を拡張できないことです。
これを行うには、type
と &
を使用する必要があります。
class
は、interface
または type
のいずれかを実装できます。
class StateT implements TState {
name : string = '' ;
capital : string = '' ;
}
class StateI implements IState {
name : string = '' ;
capital : string = '' ;
}
このように類似点をあげましたが、違いはどうでしょう?
union type がありますが、union interface はありません。
type AorB = 'a' | 'b' ;
入力変数と出力変数に別々のタイプがあり、名前から変数へのマッピングがある場合は、union型で拡張すると便利です。
type Input = { /* ... */ };
type Output = { /* ... */ };
interface VariableMap {
[ name : string ] : Input | Output ;
}
次に、変数に名前を付ける型が必要になる場合があります。
type NamedVariable = ( Input | Output ) & { name : string };
interface
でこの型を表現することはできません。
type
は interface
よりも機能が豊富です。
union 型にすることもでき、map 型や条件付き型などのより高度な機能を利用することもできます。
また、tuple 型 と配列型をよりシンプルに表現できます。
type Pair = [ number , number ];
type StringList = string [];
type NamedNums = [ string , ... number []];
interface
を使用して tuple 型 のようなものは表現できます。
interface Tuple {
0 : number ;
1 : number ;
length : 2 ;
}
const t : Tuple = [ 10 , 20 ]; // OK
しかし、これは厄介なことがあります。
concat
のような全ての tuple メソッドを削除してしまうので、type
を使用する方が良いです。
ただし、interface
には、type
には無いいくつかの機能があります。
そのひとつは、interface
を拡張できることです。
IState
の例に戻ると、別の方法で人口フィールドを追加できます。
interface IState { name : string ; capital : string ; }
interface IState { population : number ; }
const wyoming : IState = {
name : 'Wyoming' ,
capital : 'Cheyenne' ,
population : 500000
}; // OK
これは、declaration merging と呼ばれるもので、見慣れないものであった場合なら驚くことでしょう。
これは主に .d.ts
ファイルで使用されます。
作成する場合は、標準に従って interface
を使用します。
目的は、利用者が入力する必要がある型宣言にギャップがあるかもしれないので、interface
を使って型宣言を行いうことです。
TypeScript はマージを使用して、JavaScript の標準ライブラリのさまざまなバージョンのさまざまな型を取得します。
たとえば、Array interface は lib.es5.d.ts
で定義されています。
デフォルトでは、これが全てです。
ただし、tsconfig.json
の lib エントリに ES2015 を追加すると、TypeScript には lib.es2015.d.ts
も含まれます。
これには、ES2015 で追加された find などの追加メソッドを備えた別の Array interface が含まれます。
それらは、マージによって他の Array interface に追加されます。
最終的には、正確な配列型を単一取得できます。
マージは .d.ts
ファイル だけでなく通常のコードでもサポートされており、その副作用には注意しなければいけません。
自分が定義した型に追加されないことが不可欠である場合は、type
を使用します。
まとめ
ところで、type
、interface
は結局どちらを使えば良いでしょうか?
複雑な型の場合、選択肢はありません。
type
を使用する必要があります。
しかし、どちらの方法でも表現できる単純なオブジェクト型はどうでしょうか。
このとき、一貫性と拡張性を考慮する必要があります。
一貫して interface
を使用するコードベースで作業していますか?
それなら、interface
に統一します。
スタイルが確立されていないプロジェクトの場合、拡張性について検討する必要があります。
API の型宣言を公開している場合は、API が変更されたときに、利用者が interface
を介して新しいフィールドにマージできるようになると便利なので interface
を使用します。
ただし、プロジェクトで内部的に使用される型の場合、宣言のマージによって想定外な型になっている可能性があるので type
を優先した方が良さそうです。
出典: Dan Vanderkam(著), 「Effective TypeScript」, O'Reilly Media; 1版 (2019/10/17), Item 13: Know the Differences Between type and interface