はじめまして!花王株式会社の@yomu_n と申します。
「Kaoエンジニアコミュニティβ」のメンバーとして参加します。よろしくお願いします!
先日チーム内で「TypeScriptの型定義にはinterfaceとtypeのどちらを使うべきか?」という話題になりました。
このトピックは議論され尽くしている印象がありますが、あえて改めて調査して整理してみることにしました。
TypeScriptでは、型定義にinterface
とtype
の2つの方法があります。どちらを使うべきかは、プロジェクトの規模や目的によって異なります。
この記事では、それぞれの特徴と使い分けのポイントを整理し、実際のコード例や公式ドキュメント、コミュニティの見解を交えて解説します。
TypeScript公式ドキュメントでは、interface
とtype
は多くのケースで同等に使用できるとされています。
しかし、特定の状況では一方が他方より適している場合があります。
重要なのは、プロジェクト内で一貫したルールを定め、チーム全体で統一することです。
Differences Between Type Aliases and Interfaces – TypeScript Handbook
宣言と代入の違い
// interface
interface IHoge {
hoge: string;
hogehoge: string;
}
// type
type THoge = {
hoge: string;
hogehoge: string;
};
const tHoge: THoge = { hoge: 'hoge1', hogehoge: 'Thogehoge' };
const iHoge: IHoge = { hoge: 'hoge2', hogehoge: 'Ihogehoge' };
両者は似ていますが、type
はより広範な型を定義できるという点で柔軟性があります。
// プリミティブ型のエイリアス
type UserId = string;
// ユニオン型
type Status = "loading" | "success" | "error";
// タプル型
type Coordinate = [number, number];
// 関数型
type UpdateFn = (id: string, value: number) => boolean;
// 条件型
type MaybeT> = T extends null | undefined ? never : T;
拡張性とマージ
interface
は同名の宣言が自動的にマージされ、拡張が容易です。これはライブラリの型定義などで便利です。
interface Hoge {
x: number;
y: number;
}
interface Hoge {
z: string;
}
const ok: Hoge = { x: 1, y: 1, z: 'p1' }; // OK
const ng: Hoge = { x: 1, y: 1 }; // コンパイルエラー
一方、type
では同名の再定義はエラーになります。ただし、インターセクション型(&
)を使って型を組み合わせることは可能です。
type Hoge = {
x: number;
y: number;
};
type Hoge = { // コンパイルエラー
z: string;
};
type THoge = {
hoge: string;
hogehoge: string;
};
type THoge2 = THoge & {
id: number;
};
ただし、interface
のマージ機能は柔軟な一方で、意図しない型の肥大化や競合が起こるリスクもあります。
特に大規模なコードベースや複数人開発では注意が必要です。
本記事では概要を中心に整理しましたが、より詳細な仕様の違いや特殊ケースについて知りたい場合は、hsato_workmanさんによる以下のZenn記事がとても参考になります。
Zenn: TypeScriptにおけるinterface vs type(hsato_workman)
interface
が適している場合
- オブジェクトの構造を定義し、拡張やマージが必要な場合
- クラスの実装(
implements
)に使用する場合 - ライブラリやフレームワークの公開APIの型定義
type
が適している場合
- ユニオン型、タプル型、プリミティブ型のエイリアス
- 条件型やマッピング型など、高度な型操作が必要な場合
- 関数型やリテラル型の定義
かつて @typescript-eslint
には prefer-interface
というルールが存在し、オブジェクト型の定義には type
より interface
を使うべきだというスタンスが取られていました。
しかしこのルールは、バージョン 2.2.0(2020年)で非推奨となり、後にパッケージから削除されています。
→ Issue #433
現在は、代替として @typescript-eslint/consistent-type-definitions
が提供されています。
これは、プロジェクトごとに type
または interface
のどちらかを統一的に使うためのルールです。
→ consistent-type-definitions – ESLintルール解説
TypeScriptでは、オブジェクトの型チェックにおいて interface
と type
は似たように使えるように見えますが、実際には「代入できるかどうか」の挙動が異なるケースがあります。
interface はより厳格
interface
を使ってインデックスシグネチャ(任意のキーに対して特定の型を指定)を定義した場合、すべてのプロパティがその指定に従っている必要があります。
export interface Params {
[name: string]: string;
}
interface MyParams {
prop: string;
}
const myParams: MyParams = { prop: 'X' };
const params: Params = myParams; // エラーになる
この例では Params は「どんなキーでも string 型の値であること」が求められるのに対して、MyParams の構造だけではそれを満たしているとは言い切れないため、型エラーになるようです。
type は構造的に緩やか
一方で、同じような構造を type で定義した場合、代入が許容されることがあります。
これは TypeScriptの構造的型付け(structural typing) の特徴のひとつで、type の場合は中間変数を使った代入において柔軟な評価が行われるからのようです。
type Params = {
[name: string]: string;
};
type MyParams = {
foo: string;
};
const myParams: MyParams = { foo: "bar" };
const params: Params = myParams; // OK(ただし直接代入ではない)
このように、一見型が完全に一致していなくても「構造が compatible(互換)である」と判断されれば、type alias(型エイリアス)では代入が許可されるようです。
関連Issue: #14736 – assignability between interfaces and types
function httpService(path: string, headers: { [x: string]: string }) {}
const headers = {
"Content-Type": "application/x-www-form-urlencoded"
};
httpService("", { "Content-Type": "application/x-www-form-urlencoded" }); // OK
httpService("", headers); // 現在はOKだが、以前はエラー
この挙動は、PR #7029 により修正され、変数経由の代入も許容されるようになりました。
使用目的 | 推奨される型定義方法 |
---|---|
オブジェクトの構造定義 | interface |
クラスの実装(implements ) |
interface |
ユニオン型、タプル型、プリミティブ型 | type |
条件型、マッピング型などの高度な型操作 | type |
型の拡張やマージが必要な場合 | interface |
最初に挙げた「どちらを使うべきか?」という問いに対しては、万能な正解はありませんが、本記事で示したような観点をもとに、場面ごとの判断とチーム内での統一を意識することが重要です。
一貫性を保ちながら、可読性と保守性の高いコードを目指しましょう。
TypeScript公式ドキュメント
ESLint関連
スタイルガイド・解説書
ブログ記事・コミュニティ解説
Views: 0