【TypeScript】interface と type の違い・使い分け完全ガイド|宣言マージ・拡張・union型まで実例で解説

TypeScript を書いていると必ず出てくる疑問が「interface と type、どちらを使えばいいの?」です。どちらもオブジェクトの形(型)を定義できますが、できることに違いがあります。

「とりあえず interface を使っている」「なんとなく type を使っている」という方も多いですが、違いを正しく理解することで、より意図が明確で保守しやすいコードが書けるようになります。本記事では interface と type の機能の違いを比較しながら、実務での使い分けを体系的に解説します。

この記事で学べること

  • interface と type それぞれの基本構文と特徴
  • interface だけが持つ「宣言マージ(Declaration Merging)」の仕組み
  • 拡張方法の違い(extends vs &)
  • type だけが使える union 型・交差型・mapped type・conditional type
  • パフォーマンスとエラーメッセージの観点からの違い
  • 実務での使い分け判断フロー
  • よくある誤解とエラーの解決方法

前提知識:TypeScript の基本型(string, number など)と型の基礎を理解していることを前提とします。

スポンサーリンク

基本構文の比較

まず、同じ「ユーザー型」を interface と type それぞれで定義した例を見てみましょう。

interface と type — 同じことができる基本例
// interface で定義
interface UserInterface {
  id:    number;
  name:  string;
  email: string;
  greet(): void;
}

// type で定義
type UserType = {
  id:    number;
  name:  string;
  email: string;
  greet(): void;
};

// どちらも同じように使える
const u1: UserInterface = { id: 1, name: "Alice", email: "a@example.com", greet() {} };
const u2: UserType     = { id: 1, name: "Alice", email: "a@example.com", greet() {} };

オブジェクトの形を定義する場合、基本的な機能は interface と type で同じです。どちらを使っても型チェックの動作に差はありません。違いが出るのは「特殊な機能を使うとき」です。

interface の特徴

① extends による拡張(継承)

interface は extends キーワードで別の interface を拡張できます。複数の interface を同時に extends することも可能です。

interface の extends
interface Animal {
  name: string;
  eat(): void;
}

interface Pet {
  owner: string;
}

// 複数の interface を extends
interface Dog extends Animal, Pet {
  breed: string;
  bark(): void;
}

// Dog は Animal + Pet + 固有プロパティを持つ
const dog: Dog = {
  name: "ポチ", owner: "田中", breed: "柴犬",
  eat() {}, bark() {},
};

② 宣言マージ(Declaration Merging)

interface の最大の特徴が宣言マージです。同じ名前の interface を複数回定義すると、TypeScript が自動的にマージします。

宣言マージ — interface だけの機能
interface Config {
  host: string;
}

// 同名 interface を再定義 → 自動マージされる
interface Config {
  port: number;
}

// Config は { host: string; port: number; } として扱われる
const cfg: Config = { host: "localhost", port: 3000 };

// type では同名定義はエラーになる
// type Config = { host: string };     // OK
// type Config = { port: number };     // NG: Duplicate identifier 'Config'

宣言マージの主な用途:ライブラリの型定義(.d.ts)を拡張する場面でよく使われます。例えば Express の Request に独自プロパティを追加する「モジュール拡張(Module Augmentation)」は宣言マージを利用しています。

③ implements による実装強制(クラスとの連携)

class での implements
interface Printable {
  print(): void;
  label: string;
}

class Document implements Printable {
  label = "文書";
  print() {
    console.log(this.label);
  }
}

// type でも implements は使える(どちらも同じ)
type Serializable = { serialize(): string };
class Data implements Serializable {
  serialize() { return "data"; }
}

type の特徴

① union 型・交差型(interface では不可)

type の最大のアドバンテージは union 型(|交差型(& を直接使えることです。

union 型と交差型
// union 型(どちらかの型)
type StringOrNumber = string | number;
type Id = string | number | null;

// 交差型(両方の型を持つ)
type AdminUser = User & Admin;
// AdminUser は User と Admin のプロパティを全て持つ

// リテラル型の union も type でよく使う
type Direction = "left" | "right" | "up" | "down";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

// interface では union 型を直接定義できない
// interface Id = string | number;  // NG: 構文エラー

② プリミティブ型・タプル・配列への別名

プリミティブ・タプル・配列の型エイリアス
// プリミティブ型への別名
type UserId = string;
type Score  = number;

// タプル型
type Point = [number, number];
type RGB   = [number, number, number];
const pos: Point = [10, 20];

// 配列型の別名
type StringList = string[];
type Matrix     = number[][];

// interface ではプリミティブやタプルの別名は作れない
// interface UserId = string;  // NG

③ Mapped Types・Conditional Types との組み合わせ

高度な型操作(Mapped Types・Conditional Types)は type と組み合わせて使います。高度な型の機能をフルに活用できるのは type の強みです。

Mapped Types と Conditional Types
// Mapped Type: プロパティを変換
type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Conditional Type: 条件分岐で型を選択
type NonNullable<T> = T extends null | undefined ? never : T;

// Template Literal Types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">;  // "onClick"

④ &(交差型)による拡張

type を「拡張」する場合は交差型(&)を使います。interface の extends と似ていますが、動作が少し異なります

type の & 拡張 vs interface の extends
// type の & 拡張
type Animal = { name: string };
type Dog    = Animal & { breed: string };

// interface の extends
interface IAnimal { name: string; }
interface IDog extends IAnimal { breed: string; }

// プロパティが衝突した場合の挙動の違い
type A = { x: number };
type B = { x: string };
type AB = A & B;  // x: never(number & string = never)

// interface extends はプロパティ衝突でコンパイルエラー
// interface IA { x: number; }
// interface IB extends IA { x: string; }  // Error: x の型が互換しない

交差型と extends の違い:プロパティが衝突した場合、type &never 型(到達不能な型)を生成してコンパイルエラーになりません(実行時に問題が出る可能性があります)。一方 interface extends は衝突時にコンパイルエラーを出してくれます。継承による型の整合性を保証したい場合は interface extends の方が安全です。

interface と type の機能比較

機能 interface type
オブジェクト型の定義
クラスへの implements
extends による拡張 ○(複数可) × (代わりに &
宣言マージ ○(同名定義で自動マージ) ×(重複定義はエラー)
union 型(| ×
交差型(& ×(extends で代替)
プリミティブ型の別名 ×
タプル型・配列型 ×
Mapped Types ×(一部対応) ○(完全対応)
Conditional Types ×
Template Literal Types ×
エラーメッセージの読みやすさ ◎(名前付きで表示) △(構造が展開されやすい)
コンパイルパフォーマンス ○(キャッシュが効く) △(複雑な type は遅くなる場合あり)

実務での使い分け判断フロー

「interface と type、どちらを使うべきか」という問いへの答えは用途によって異なります。以下の判断フローを参考にしてください。

使い分けの判断フロー

  1. union 型・タプル・プリミティブの別名が必要type 一択
  2. ライブラリ型定義の拡張(Module Augmentation)が必要interface 一択
  3. クラスの設計図・公開 API の型を定義interface を推奨
  4. Mapped Type・Conditional Type を使った型変換type 一択
  5. 単純なオブジェクト型で上記に当てはまらない → チームの規約に従う(どちらでも可)

プロジェクト規模・用途別のおすすめ

場面 おすすめ 理由
ライブラリ・npm パッケージ開発 interface 宣言マージでユーザーが型を拡張できる
React コンポーネントの Props type union 型や交差型が Props で頻出
クラスの設計・OOP パターン interface implements との相性がよく意図が明確
API レスポンスの型 type union 型でエラー/成功を表現しやすい
ドメインモデル・エンティティ interface 拡張性・宣言マージが活きる
ユーティリティ型の自作 type Mapped Type / Conditional Type が必須

実務パターン

React コンポーネントの Props(type が多い)

React Props の型定義
// union 型を使うので type が適している
type ButtonProps = {
  label:    string;
  variant: "primary" | "secondary" | "danger";
  size?:   "sm" | "md" | "lg";
  disabled?: boolean;
  onClick?: () => void;
};

function Button({ label, variant, size = "md", disabled = false, onClick }: ButtonProps) {
  return `<button class="${variant} ${size}" disabled=${disabled}>${label}</button>`;
}

ライブラリ型の拡張(interface の宣言マージ)

Express の Request を拡張する例
// express.d.ts(型定義ファイル)
// Express の Request に user プロパティを追加
import "express";

declare module "express" {
  interface Request {
    user?: {
      id: string;
      role: string;
    };
  }
}

// これで router.get 内で req.user.id が使えるようになる
// ※ type では宣言マージできないためこの用途は interface のみ

API レスポンスの型(type + union 型)

API レスポンスを union 型で型安全に扱う
// 成功・失敗を union 型で表現
type ApiResult<T> =
  | { success: true;  data: T       }
  | { success: false; error: string };

async function getUser(id: number): Promise<ApiResult<User>> {
  try {
    const data = await fetchUser(id);
    return { success: true, data };
  } catch (e) {
    return { success: false, error: String(e) };
  }
}

// 呼び出し側で型が絞り込まれる
const result = await getUser(1);
if (result.success) {
  console.log(result.data.name);   // User 型
} else {
  console.error(result.error);
}

よくある誤解とエラー

よくある誤解・NG パターン
// ❌ 誤解① interface で union 型を作ろうとする
// interface Status = "active" | "inactive";  // NG: 構文エラー
// ✅ 正解: type を使う
type Status = "active" | "inactive";

// ❌ 誤解② 同名 type を再定義しようとする
// type Config = { host: string };
// type Config = { port: number };  // NG: Duplicate identifier 'Config'
// ✅ 正解: interface で宣言マージ、または交差型を使う
interface Config { host: string; }
interface Config { port: number; }  // OK: マージされる

// ❌ 誤解③ & 拡張でプロパティ衝突を見落とす
type A = { x: number };
type B = { x: string };
type C = A & B;  // x: never — コンパイルエラーにならないが使えない
const c: C = { x: 1 };  // Error: number は never に代入不可

まとめ

interface vs type — 結論

  • union 型・タプル・プリミティブ別名が必要type
  • 宣言マージが必要(ライブラリ拡張など)interface
  • Mapped / Conditional / Template Literal Typestype
  • クラスの implements・OOP 設計interface(どちらでも動くが意図が明確)
  • どちらでもよい単純なオブジェクト型 → チーム規約に統一(TypeScript 公式は interface を推奨)
  • プロパティが衝突する可能性がある拡張 → interface extends(コンパイルエラーで検知できる)

型定義をより深く学ぶには、高度な型(Mapped Type・Conditional Type)ユーティリティ型(Partial・Pick・Omit)もあわせて確認してください。クラスの型定義についてはクラスの型定義 完全ガイドもご参照ください。

よくある質問(FAQ)

TypeScript 公式はどちらを推奨していますか?

公式ドキュメントでは「可能な限り interface を使い、union 型など interface で表現できない場合に type を使う」というガイドラインを示しています。ただし、これは強制ではなくあくまで推奨です。React エコシステムでは Props に type を使うことが慣例になっています。

interface の extends と type の & は何が違いますか?

主な違いはプロパティが衝突した場合の動作です。interface extends は衝突時にコンパイルエラーを出しますが、type & は衝突プロパティが never 型になりエラーが遅延されます。継承の整合性を型レベルで保証したい場合は extends の方が安全です。

パフォーマンスの違いはありますか?

複雑な type(特に多段の Mapped Type や Conditional Type)はコンパイル時の型チェックが遅くなる場合があります。interface はキャッシュが効きやすいため、大規模プロジェクトでは interface の方がコンパイルが速い傾向があります。ただし、通常の開発では気にならないレベルです。

既存コードで interface と type が混在しています。統一すべきですか?

機能上の問題がなければ無理に統一する必要はありません。ただし、チーム内で規約を決めておくと保守性が上がります。おすすめは「オブジェクト型は interface、それ以外(union 型・ユーティリティ型)は type」という使い分けです。