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、どちらを使うべきか」という問いへの答えは用途によって異なります。以下の判断フローを参考にしてください。
使い分けの判断フロー
- union 型・タプル・プリミティブの別名が必要 →
type 一択
- ライブラリ型定義の拡張(Module Augmentation)が必要 →
interface 一択
- クラスの設計図・公開 API の型を定義 →
interface を推奨
- Mapped Type・Conditional Type を使った型変換 →
type 一択
- 単純なオブジェクト型で上記に当てはまらない → チームの規約に従う(どちらでも可)
プロジェクト規模・用途別のおすすめ
| 場面 |
おすすめ |
理由 |
| ライブラリ・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 Types →
type
- クラスの 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」という使い分けです。