TypeScript で type と interface どっち使うのか問題
はじめに
あくまで一個人の意見なので絶対的な解ではないというのと、どっちをデフォルトに選んでも普通にアプリケーション開発してて困ることはほぼほぼないと思うので、そこまで気を揉むことでもない、ということだけ最初に述べておいて意見をしたためます。
TL;DR
- アプリケーション開発では基本的に type でおk
- Declaration merging したい時だけ interface
- ライブラリ開発のような使う側で拡張したい(Declaration merging したい)時は interface
- とりあえずチームでどっちをデフォルトにするかは統一しといた方が気持ちいい
type と interface の違い
機能的にはそんなに大きな違いはなく、個人的に判断に関わるのは次の3つかなと思います。
- interface では Declaration merging がされる。type ではされない
- type では union 型や tuple 型が作れる。interface では作れない
- interface で定義したものは index signature で定義したオブジェクトに代入できない
1.interface では Declaration merging がされる。type ではされない
そもそも Declaration merging ってなんやねんという方のために説明すると、ざっくり言うと同じ名前の型(interface)定義が複数ある時に自動的に一つの定義にマージしてくれる機能のことです。
interface Person {
name: string
}
interface Person {
email: string
}
// name と email 両方必要になる
const john: Person = {
name: "john",
email: "hoge@example.com"
}
外部のライブラリの型なんかを名前を変えないまま拡張したい時などに便利な機能です。
ただデメリットとして、たまたまグローバルに定義されている型と同じ名前をつけてしまい拡張してしまうことが挙げられます。
このデメリットそんなに発動する機会ないのですが、Declaration merging が必要になる機会もほぼないと思うので、普段は type 使う方に倒しといた方がとりあえず safe なんじゃないのというのが個人的な意見です。
2.type では union 型や tuple 型が作れる。interface では作れない
こういったものです。
// Union
type Hoge = Fuga | Piyo
// tuple
type Hoge = [number, string]
こういうのを表現したい時は type
を使うしかないです。
3.interface で定義したものは index signature で定義したオブジェクトに代入できない
そもそも index signature とはなんぞやという話ですが、次のようにオブジェクトの構造が規則に則っていることを規定できる型です。
// https://typescript-jp.gitbook.io/deep-dive/type-system/index-signatures から引用
let foo:{ [index:string] : {message: string} } = {};
/**
* Must store stuff that conforms to the structure
*/
/** Ok */
foo['a'] = { message: 'some message' };
/** Error: must contain a `message` or type string. You have a typo in `message` */
foo['a'] = { messages: 'some message' };
ここで問題なのが、interface で定義したものは index signature を用いた型に代入できないということです。
一例を挙げてみると次のような {[key: string]: string}
を引数を取るものがあるとして
declare function test(obj: {[key: string]: string}): void;
上記の obj
に対応したものを、 type と interface それぞれで定義して代入してみようと思います。
type だと問題なくコンパイルできます。
type TestObjectType = {
text: string,
}
const testObject: TestObjectType = {
text: "mozi"
};
test(testObject) // 問題なし
ですが interface だと Index signature is missing in type 'TestObjectInterface'.ts(2345)
というエラーが出ます。
interface TestObjectInterface {
text: string,
}
const testObject: TestObjectInterface = {
text: "mozi"
};
// Index signature is missing in type 'TestObjectInterface'.ts(2345)
test(testObject)
interface の場合は先ほど紹介した Declaration merging を待つため、型の中身が確定していません。今見えてる範囲では {[key: string]: string}
を満たしていても、拡張されて number のプロパティが追加されるかもしれません。
あまり頻度は高くないかもしれないですが、これも interface を使う一つのデメリットなのかなと思います。
コメントを受けての追記
というのを interface のデメリットとして挙げましたが、どうも interface の挙動の方がより堅牢で私の解釈が間違っていたようです。
詳しくはこの記事の acomagu さんのコメントを参照いただければと思いますが、 index signature の型のオブジェクトに対して type で定義したオブジェクトでは index signature で定義した型でないプロパティも代入できてしまうようです。
まとめ
以上から type に統一しておいた方が困ることは少なくなるのではと思っているので、個人的には type をデフォルト推しです。
参考にした記事・動画
-
TypeScript Interfaces vs Types
- この方は interface 派。個人的には拡張性云々に関しては interface の extends は type の
A & B
でマージするのと変わらないのでそんなに差はないと思っておる- (コメントより) interface の extends を使う方が type の union 型を使うよりビルド時のキャッシュが効いてパフォーマンスが優れているそうです。ビルド時間が遅い時の改善策として知っておくと良さそうです。
- https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections
- この方は interface 派。個人的には拡張性云々に関しては interface の extends は type の
- Tidy TypeScript: Prefer type aliases over interfaces
- TypeScriptのInterfaceとTypeの比較
- Types vs. interfaces in TypeScript
Discussion
自分は少し違うと考えています。この場合単に(Declaration merging は関係なく) interface の挙動のほうが安全で、type の挙動は安全性を犠牲にして(便利さを取って)いると思っています。
例えば
このコードは test の引数に
num: 1
が入ってしまっているにも関わらず、コンパイルが通ってしまいます。interface を使っていればこれを回避できます。Typescript の Structual Subtyping の性質(type/interfece ともに定義されていないフィールドに関しては何でも受け入れます)を考えれば、interface の挙動のほうが自然に感じます。
ちなみにこの挙動に関しては GitHub の Issue で議論されています: https://github.com/microsoft/TypeScript/issues/15300
ご指摘ありがとうございます。
なるほど…確かにinterfaceの挙動の方が安全に感じますね。記事の内容は後ほど修正しようと思います。
イシューのリンクもありがとうございます!
type A
とtype B
がそれぞれinterface
かtype
かに関わらず、const a: A = b
は成功するので、num: 1
がエラーなくtest()
に入ってしまいます。安全側に倒すならtype A
とinterface B
のときにA = b
がエラーになるべきと思いますが、現状そうなっていません。ただ、オブジェクトリテラル型
{ text: string }
がtextのみ持つ型の表現である(下記参照)以上、{ text: string }
に対して{ text: "abc", num: 1 }
も飛んでくる引数等では、interfaceを使っておく方がより安全と言えると思います。整理すると下記のように理解しています。
type MappedType = { [key: string]: string }
:全てのプロパティに対する値がstringである型interface IA { text: string }
:最低限textというプロパティを持つ型type A = { text: string }
:text
というプロパティのみを持つ型(オブジェクトリテラル型)type A
はtext: string以外のプロパティを持っていない(はずな)ので、MappedType
に代入しても問題ないですが、interface IA
は他のプロパティを持ちうるので代入できません。Index signature is missing in type
はわかりづらいので別のエラーメッセージになると良さそうですよね。オブジェクトリテラル型は本来
const foo = { text: 'a' }
と書いた時のfooの型なので、実際の値は他のプロパティを持たない想定です。よって型のプロパティを全て見ることでindex signature(この場合は[key: string]: string
)が判定できます(checker.tsのisObjectTypeWithInferableIndex()
を参照)。(ちなみにtypeがガバガバでよくない型なのではなく、オブジェクトリテラル型にtype Aという別名をつけている状態です。)
今後状況は変わるかもしれませんが、2つ以上の型の拡張に関してはtype aliasによる交差型を作成するよりも、interfaceの
extends
を利用するほうがビルド時のキャッシュが効くとWikiに記載があります。ケースによってはinterface
のほうがビルドパフォーマンスの観点からすると優れている場合があると考えられます。パフォーマンスは観点から抜けていました
Twitterの方でもコメントありがとうございました!