2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GameWithAdvent Calendar 2019

Day 16

名前付き型を interface と type で定義するときの留意事項

Last updated at Posted at 2019-12-16

TypeScriptの interfacetype の違い

TypeScriptを採用する最大の利点は、強力な型システムにあります。
型の名前を定義する typeinterface の2つのオプションには、類似点が多いです。

プロジェクトでどちらかを使う上で迷いが出てしまうことがあるので、これらのオプションについて特性を調べました。

TypeScript v3.7.2 の Playground で検証しています。

TL;DR

  • typeinterface の類似点と違いを理解します。
  • いずれかの構文を使用して同じ型を記述する方法を知る。
  • プロジェクトでどちらを使用するかを決める際には、確立されたスタイルと拡張性が有用かどうかを検討する必要がある。

類似点と違い

TypeScriptで型の名前を定義する。

type TState = { name : string ; capital : string ; } 
interface IState { name : string ; capital : string ; } 

普段どちらを選択しているでしょうか?

これら2つのオプションの境界線は、長年にわたってますます曖昧になってきています。多くの場面で両方とも利用できます。

型は互いにほとんど区別できません。

未定義プロパティを使用したときに発生するエラー内容は、 IStateTStateで同様です。

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' 
};

typeinterface でどちらも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 は型を拡張できます。
また、typeinterface を拡張できます。

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 でこの型を表現することはできません。

typeinterface よりも機能が豊富です。

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 を使用します。

まとめ

ところで、typeinterface は結局どちらを使えば良いでしょうか?

複雑な型の場合、選択肢はありません。

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?