20
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめまして!花王株式会社の@yomu_n と申します。
「Kaoエンジニアコミュニティβ」のメンバーとして参加します。よろしくお願いします!

はじめに

先日チーム内で「TypeScriptの型定義にはinterfaceとtypeのどちらを使うべきか?」という話題になりました。
このトピックは議論され尽くしている印象がありますが、あえて改めて調査して整理してみることにしました。

TypeScriptでは、型定義にinterfacetypeの2つの方法があります。どちらを使うべきかは、プロジェクトの規模や目的によって異なります。
この記事では、それぞれの特徴と使い分けのポイントを整理し、実際のコード例や公式ドキュメント、コミュニティの見解を交えて解説します。

結論:明確な正解はないが、使い分けの指針は存在する

TypeScript公式ドキュメントでは、interfacetypeは多くのケースで同等に使用できるとされています。
しかし、特定の状況では一方が他方より適している場合があります。

重要なのは、プロジェクト内で一貫したルールを定め、チーム全体で統一することです。
Differences Between Type Aliases and Interfaces - TypeScript Handbook

interfacetypeの基本的な違い

宣言と代入の違い

// 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 Maybe<T> = 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が適している場合

  • ユニオン型、タプル型、プリミティブ型のエイリアス
  • 条件型やマッピング型など、高度な型操作が必要な場合
  • 関数型やリテラル型の定義

ESLintルールの変遷とその意味

かつて @typescript-eslint には prefer-interface というルールが存在し、オブジェクト型の定義には type より interface を使うべきだというスタンスが取られていました。

しかしこのルールは、バージョン 2.2.0(2020年)で非推奨となり、後にパッケージから削除されています。
Issue #433

現在は、代替として @typescript-eslint/consistent-type-definitions が提供されています。
これは、プロジェクトごとに type または interface のどちらかを統一的に使うためのルールです。
consistent-type-definitions - ESLintルール解説

公式ドキュメントやスタイルガイドの見解

補足:TypeとInterfaceの代入可能性の違い

TypeScriptでは、オブジェクトの型チェックにおいて interfacetype は似たように使えるように見えますが、実際には「代入できるかどうか」の挙動が異なるケースがあります。

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関連

スタイルガイド・解説書

ブログ記事・コミュニティ解説

20
6
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
20
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?