はじめに
この記事では、TypeScript における interface と type の違いや使い分けのポイント、実務での活用方法などについてサンプルコードを交えながらまとめます。
interface と type とは?
TypeScript で型を定義する方法として、interface と type の2つが用意されています。
どちらもオブジェクトの形を定義できますが、それぞれに得意・不得意があり、適切な場面で使い分けることで、より型安全で保守性の高いコードを書くことができます。
▼ 例:オブジェクトの型定義
interface User {
id: number;
name: string;
}
// 推論: interface User
type User = {
id: number;
name: string;
};
// 推論: type User
基本的な書き方はほぼ同じですが、重要な違いがいくつかあります。
主な違い
1. 宣言のマージ(Declaration Merging)
interface は同じ名前で複数回宣言すると、自動的にマージされます。
interface User {
name: string;
}
interface User {
age: number;
}
const user: User = {
name: "太郎",
age: 25
};
// 推論: User は { name: string; age: number; } として扱われる
一方、type は同じ名前で複数回宣言できません。
type User = {
name: string;
};
type User = {
age: number;
};
// ❌ エラー: 重複した識別子 'User'
2. ユニオン型の定義
type はユニオン型を直接定義できますが、interface ではできません。
// ✅ type ではユニオン型を定義可能
type Status = "pending" | "success" | "error";
// 推論: "pending" | "success" | "error"
type Result = SuccessResult | ErrorResult;
// 推論: SuccessResult と ErrorResult のユニオン型
// ❌ interface ではユニオン型を定義できない
interface Status = "pending" | "success" | "error";
// 構文エラー
3. 継承と拡張
interface は extends キーワードで継承できます。
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
// 推論: Dog は name と bark を持つ
type は &(インターセクション型)を使います。
type Animal = {
name: string;
};
type Dog = Animal & {
bark(): void;
};
// 推論: Dog は name と bark を持つ
使い分けの参考ポイント
以下のようなポイントが使い分けの参考になります。
| シチュエーション | 推奨 | 理由 |
|---|---|---|
| オブジェクトの型定義 | interface |
拡張性が高く、宣言のマージが可能 |
| ユニオン型 | type |
interface では定義不可 |
| プリミティブ型のエイリアス | type |
interface では定義不可 |
| 関数の型定義 | どちらでも | チームで統一すればOK |
| ライブラリの公開API | interface |
利用者が拡張できる |
開発者の認知負荷軽減のため、シチュエーションごとに厳密に使い分けることはせず、チームでどちらかに統一するという考えもあると思います。
ちなみに今の私が所属しているチームでは、基本的に type を使い、interface を使わないと実現できない型定義だけ interface を使うという方針にしています。
▼ 使用例1: APIレスポンスの型定義
// オブジェクトの構造を定義するので interface を使用
interface ApiResponse<T> {
data: T;
total: number;
page: number;
}
interface User {
id: number;
name: string;
email: string;
}
const response: ApiResponse<User[]> = {
data: [{ id: 1, name: "太郎", email: "taro@example.com" }],
total: 100,
page: 1
};
// 推論: ApiResponse<User[]>
▼ 使用例2: ステート管理(ユニオン型)
// ユニオン型を含むので type を使用
type LoadingState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; error: Error };
// 推論: 4つの状態のユニオン型
// オブジェクトの型定義は interface
interface AppState {
user: User | null;
loadingState: LoadingState;
}
▼ 使用例3: Reactコンポーネントのprops
// オブジェクトの型定義なので interface を推奨
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
disabled?: boolean;
}
// 推論: interface ButtonProps
const Button: React.FC<ButtonProps> = ({
label,
onClick,
variant = "primary",
disabled
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={variant}
>
{label}
</button>
);
};
ESLint で統一を強制できます。
@typescript-eslint/consistent-type-definitions ルールを使うと、interface と type の使い分けを自動チェックできます。
{
"rules": {
"@typescript-eslint/consistent-type-definitions": ["error", "interface"]
}
}
このルールを設定すると、オブジェクトの型定義で type を使った場合にエラーが出るようになります。
実践的な活用方法
ライブラリの型定義を拡張する
interface の宣言のマージを利用すると、ライブラリの型定義を拡張できます。
// 例: Windowオブジェクトにカスタムプロパティを追加
declare global {
interface Window {
gtag: (...args: any[]) => void;
dataLayer: any[];
}
}
// これで型エラーなく使える
window.gtag('event', 'page_view');
// 推論: Window.gtag は関数として認識される
複雑な型の組み合わせ
type を使うと、複雑な型操作が可能です。
type Nullable<T> = T | null;
// 推論: T または null
type ReadonlyDeep<T> = {
readonly [K in keyof T]: T[K] extends object
? ReadonlyDeep<T[K]>
: T[K];
};
// 推論: 再帰的に readonly にする型
type User = {
id: number;
profile: {
name: string;
age: number;
};
};
type ImmutableUser = ReadonlyDeep<User>;
// 推論: すべてのプロパティが readonly
まとめ
interface と type はそれぞれに特徴があり、適切に使い分けることで型安全性と保守性を高められます。