オブジェクトの形を表すという点では、interface と type は同じ仕事をし、形が一致すれば一方の値をもう一方に代入できます。違いは周辺部分にあります。type はインターフェースでは名前を付けられないもの(ユニオン型、タプル、計算で作る型)に名前を付けられ、インターフェースには型エイリアスにできないことがいくつかあります(マージ、チェックされる extends)。
TypeScriptはオブジェクト型を構造で比較するので、代入可能性に宣言の名前は関係ありません。以下では、選択が意味を持つ場面を並べます。
比較表
| 機能 | interface | type |
|---|---|---|
| オブジェクトの形 | はい | はい |
| 省略可能、readonly、メソッド、インデックスシグネチャ | はい | はい |
| ジェネリクス | はい | はい |
ユニオン型(A | B) | いいえ | はい |
| タプル、プリミティブ、単独の関数型 | いいえ(関数型は呼び出しシグネチャとしてだけ) | はい |
| マップ型と条件型 | いいえ | はい |
| 拡張 | extends、衝突はエラー | &、衝突は never になる |
| 宣言のマージ | はい | いいえ(識別子の重複) |
Record<string, T> への代入 | いいえ | はい、プロパティが合っていれば |
クラスでの implements | はい | はい、オブジェクト型なら |
| 再帰的な定義 | はい | はい |
type にしかできないこと
1つのオブジェクトの形ではないものには、すべて型エイリアスが必要です:
どれも interface では書けません(最後の2つは マップ型 と 条件型 で扱います)。オブジェクトの形に何を使うにせよ、どのコードベースもどこかで type を使うことになる実際的な理由がこれです。
interface にしかできないこと: 宣言のマージ
同じスコープにある同じ名前の2つの interface 宣言は1つに結合されます。同じ名前の2つの type 宣言はエラー TS2300 Duplicate identifier です。
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const s: Settings = { theme: "dark", fontSize: 14 }; // needs both
type Options = { a: number };
type Options = { b: number }; // error TS2300: Duplicate identifier 'Options'.
マージは、ライブラリの型を外から拡張する方法です。グローバルの Window、Express の Request、ライブラリのテーマの型にプロパティを追加するといった使い方です。利用者が拡張する必要があるかもしれない型を公開するなら、インターフェースを使いましょう。自分のアプリケーションのコードでは、意図しないマージ(2つのスクリプトファイルが同じグローバルのインターフェース名を宣言する)は、同じプロパティを違う型で宣言しない限り黙って起きます。これが、一部のチームが type を好む理由の1つです。
extends と交差型
インターフェースは extends で、型エイリアスは & で拡張します。たいていは同じ結果になりますが、プロパティが衝突したときの扱いが違います。extends は宣言の位置で衝突を報告します:
index.ts(7,11): error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.
交差型は同じ衝突を黙って受け入れ、プロパティを never(number でもある string)にします。エラーが表れるのは、あとで値を作ろうとしたときです:
エラーは、原因となった宣言から離れたオブジェクトの位置を指します。オブジェクト型からオブジェクト型を作るなら、extends のほうがよいメッセージを出してくれます。
インデックスシグネチャ: 見落としやすい違い
オブジェクト型の型エイリアスには暗黙のインデックスシグネチャが付くので、Record<string, unknown> が期待される場所に渡せます。インターフェースには付きません。これは設計上の判断です。TypeScriptチームの説明(TypeScriptリポジトリの issue #15300)によると、インターフェースはあとの宣言で拡張されうるので、インデックスシグネチャを推論するのは安全性が低く、今ルールを変えると壊れるコードが多すぎるということです。
@ts-expect-error を付けた呼び出しも実行され、Alan のフィールドを出力します。これはコンパイル時のルールだからです。Record<string, ...> と型付けされたログ、シリアライズ、クエリのヘルパーにインターフェースの値を渡したときに、わかりにくいエラーが出るよくある理由がこれです。その宣言だけを type に変えるか、値をスプレッドするか、ヘルパーの引数をインターフェースやジェネリクスで型付けしましょう。
パフォーマンスとエラーメッセージ
TypeScript wiki の Performance のページ(「Preferring Interfaces Over Intersections」の節)は、オブジェクト型を組み合わせるとき、type Foo = Bar & Baz & { ... } より interface Foo extends Bar, Baz { ... } を勧めています。理由は、インターフェースはプロパティの衝突を検出する1つのフラットなオブジェクト型であること、インターフェース間の型の関係はキャッシュされる(交差型全体はキャッシュされない)こと、そして交差型に対する値のチェックは、平らにした型の前にすべての構成要素をチェックすることです。この違いが効いてくるのは、組み合わせた型の多い大きなコードベースです。普通のオブジェクトの形がいくつかある程度なら、測定できるほどの差はありません。
同じページには、インターフェースのほうが表示がわかりやすいことも書かれています。インターフェースはホバーやエラーメッセージで名前で表示されますが、交差型のエイリアスは構成要素に展開されて表示されることが多く、長いメッセージが読みにくくなります。
どちらを使うか
うまくいくルール:
- オブジェクトの形:
interface。チェックされるextends、大きな組み合わせでのわかりやすいエラーが得られ、ライブラリの利用者が拡張できます。TypeScriptのハンドブックの目安「typeの機能が必要になるまではinterfaceを使う」とも一致します。 - それ以外すべて:
type。ユニオン型、タプル、関数型、リテラル型、そしてマップ型、条件型、テンプレートリテラル型で作るものすべてです。 - 例外:
Record<string, ...>の引数に当てはまる必要があるオブジェクトの形や、あえてマージを防ぎたい場合はtypeを使います。
すべてに type を使うのも筋の通った選択で、多くのコードベースがそうしています。避けるべきなのは、両方をでたらめに混ぜることだけです。読む人は、その違いが意図されたものかどうか悩むことになります。
よくある質問
TypeScriptの type と interface の違いは何ですか?
どちらもオブジェクトの形を表し、その用途では交換可能です。type はユニオン型、タプル、プリミティブ、マップ型や条件型にも名前を付けられますが、interface にはできません。interface は宣言のマージと、プロパティの衝突をチェックする extends に対応しています。また、type で書いたオブジェクト型は Record<string, unknown> のようなインデックスシグネチャの型に代入できますが、インターフェースはできません。
type と interface のどちらを使うべきですか?
TypeScriptのハンドブックの目安は、type にしかない機能が必要になるまで interface を使うことです。実際には、オブジェクトの形にはインターフェースを、ユニオン型、タプル、関数型、計算で作る型には type を使うことになります。すべてに type を使うチームもうまくやっています。大事なのは一貫したルールを1つ持つことです。
TypeScriptでは interface のほうが type より速いですか?
オブジェクト型を組み合わせる場合には、そういうこともあります。TypeScriptチームのパフォーマンスのガイダンスは、大きな交差型(A & B & { ... })より interface ... extends を勧めています。インターフェース間の関係はキャッシュされ、インターフェースは1つのフラットな型だからです。単純なオブジェクトの形なら、意味のある違いはありません。
クラスは型エイリアスを implements できますか?
エイリアスがオブジェクト型(またはオブジェクト型の交差型)ならできます。class Point implements PointType { ... } は動きます。クラスはユニオン型を実装できず、それはエラー TS2422 です。
interface は型エイリアスを継承できますか?
エイリアスがオブジェクト型ならできます。type Base = { id: string }; interface User extends Base { name: string } は正しい書き方です。ユニオン型のエイリアスは継承できません。逆方向では、型エイリアスは & でインターフェースをもとに型を作れます。