T-CREATOR

どっちを使うべき?TypeScriptのtype(型エイリアス)とinterfaceの違いと使い分け

どっちを使うべき?TypeScriptのtype(型エイリアス)とinterfaceの違いと使い分け

この記事では、type(型エイリアス)と interface の違いと、それぞれの使い方、そしてどっちを使うべき?について解説いたします。

機能対応表

type(型エイリアス)と interface がサポートする機能の可否を一覧にしています。

#機能typeinterface説明
1宣言のマージ不可可能同名宣言を重ねると自動で統合されるかどうか
2拡張(extends / &制約あり(& で合成)可能派生型を作成する手段の違い
3プリミティブ型エイリアス可能不可type ID = string のように別名を付けられるか
4ユニオン型可能不可A &#124 B を直接定義できるか
5タプル型可能制約あり[number, string] を自然に書けるか
6関数型可能可能(arg: T) => U の形を宣言できるか
7オブジェクト型可能可能プロパティを持つ構造体を定義できるか
8リテラル型可能不可文字列/数値リテラルを直接型にできるか
9条件型可能不可A extends B ? X : Y を書けるか
10型アサーション可能不可as const など型計算を伴うアサーションに使えるか
11infer キーワード可能不可条件型で部分型を抽出できるか
12typeof 演算子可能不可実値から型を取得できるか
13keyof 演算子可能不可プロパティ名を列挙したユニオンを生成できるか
14インデックスシグネチャ可能可能[K: string]: V を宣言できるか
15マッピング型可能不可[K in Keys]: T で型を変換できるか
16テンプレートリテラル型可能不可${Uppercase<T>}Id のような型計算が可能か
17再帰型可能可能自分自身を参照する型を定義できるか
18クラスによる implements不可可能class Foo implements Bar で利用できるか
19名前空間とのマージ不可可能namespace と同名で拡張できるか
20コールシグネチャのオーバーロード可能制約あり同一型で複数の関数シグネチャを持てるか

基本形の定義

1. 宣言のマージ(Declaration Merging)

解説
同じ名前の interface を複数回書くと自動で結合されます。type で同じことを行うと重複エラーになります。

ts// interface はマージされる
interface ApiContext { reqId: string }
interface ApiContext { user?: { id: number } }

const ctx: ApiContext = { reqId: 'abc', user: { id: 1 } };

// type はエラー
// type ApiContext = { reqId: string }
// type ApiContext = { user?: { id: number } } // ✅ 上と同名なので NG

2. 拡張(extends / &)

解説
interfaceextendstype は交差型 & で派生を作成します。

tsinterface Animal { name: string }
interface Dog extends Animal { bark(): void }

type Position = { x: number }
type Position3D = Position & { z: number }

3. プリミティブ型エイリアス

解説
ID などのドメイン概念を表すときは type が便利です。interface では書けません。

tstype UserId = string | number;
const id: UserId = 42;

4. ユニオン型

解説
type でしかユニオンを直接宣言できません。interface は要素側としてのみ利用します。

tstype Status = 'draft' | 'published' | 'archived';

interface Draft { status: 'draft' }
interface Published { status: 'published' }

type ArticleState = Draft | Published;

5. タプル型

解説
type なら [number, string] を自然に定義できます。interface で表す場合は添字シグネチャが必要です。

ts// 推奨
type Pair = [number, string];

// interface で無理やり書く例
interface PairLike {
  0: number;
  1: string;
  length: 2;
}

6. オブジェクト型

解説
どちらもサポートされますが、マージ前提なら interface、型演算前提なら type が向きます。

tsinterface User {
  id: number;
  name: string;
}

type ReadonlyUser = Readonly<User>;

7. 関数型

解説
シグネチャだけなら type が短く、オーバーロードを多用する場合は interface が読みやすいです。

ts// type
type ToString = (value: unknown) => string;

// interface(オーバーロード)
interface Parse {
  (s: string): number;
  (n: number): string;
}

8. リテラル型

解説
固定値の表現は type で行います。interface では直接書けません。

tstype Yes = 'yes';
type No  = 'no';

9. 条件型

解説
ジェネリックに応じて型を切り替える強力な仕組みで、type 限定です。

tstype Nullable<T> = T extends null | undefined ? never : T;

type A = Nullable<string | null>; // string

10. 型アサーション(as const 等)

解説
type を組み合わせ、リテラルを保持したまま厳密型に変換できます。

tsconst config = {
  mode: 'production',
  port: 3000,
} as const;

type Config = typeof config; // { readonly mode: "production"; readonly port: 3000 }

11. infer キーワード

解説
条件型内で部分型を抽出できます。複雑な戻り値推論に便利です。

tstype ReturnTypeOf<T> = T extends (...args: any) => infer R ? R : never;

function foo() { return { ok: true } }
type R = ReturnTypeOf<typeof foo>; // { ok: boolean }

12. typeof 演算子

解説
実値から型を取得して再利用します。

tsconst defaultUser = { id: 0, name: 'guest' };
type DefaultUser = typeof defaultUser;

13. keyof 演算子

解説
オブジェクトのプロパティ名をユニオンで取り出します。

tsinterface User { id: number; name: string }
type UserKeys = keyof User; // "id" | "name"

14. インデックスシグネチャ

解説
動的キーを持つ辞書構造を表現します。

tsinterface Dictionary {
  [key: string]: string;
}

type NumRecord = { [key: number]: boolean };

15. マッピング型

解説
既存キーを走査して新しい型を作成します。type 限定です。

tstype Optional<T> = { [K in keyof T]?: T[K] };

interface User { id: number; name: string }
type PartialUser = Optional<User>; // 全プロパティがオプション

16. テンプレートリテラル型

解説
文字列リテラルを組み合わせて新しい型を生成します。

tstype EventName<E extends string> = `${E}Changed`;

type Domain = 'user' | 'post';
type Events = EventName<Domain>; // "userChanged" | "postChanged"

17. 再帰型

解説
自己参照によりツリー構造を安全に記述できます。

tsinterface TreeNode<T> {
  value: T;
  children?: TreeNode<T>[];
}

type Json = string | number | boolean | null | Json[] | { [k: string]: Json };

18. クラスによる implements

解説
クラスが契約を満たすかコンパイル時に検証できます。interface 専用です。

tsinterface Runnable { run(): void }

class Task implements Runnable {
  run() { console.log('running'); }
}

19. 名前空間とのマージ

解説
ライブラリ拡張時に namespaceinterface を同名で定義し、静的メソッドを追加できます。

tsinterface Greeter { greet(): void }

namespace Greeter {
  export function create(): Greeter {
    return { greet() { console.log('hi'); } };
  }
}

const g = Greeter.create();
g.greet();

20. コールシグネチャのオーバーロード

解説
複数シグネチャを一つの型で表現します。type なら簡潔、interface はプロパティと混在できますが書き方がやや冗長です。

ts// type 版
type Overloaded =
  | ((n: number) => string)
  | ((s: string) => number);

// interface 版(制約あり)
interface Fn {
  (n: number): string;
  (s: string): number;
}

const f: Fn = (arg: any) =>
  typeof arg === 'number' ? String(arg) : arg.length;

どっちを使うべき?

結論を先に述べます。拡張性・互換性を重視するなら interface、型演算と柔軟な構成力を求めるなら type が最適です。以下ではメリット/デメリットを整理し、代表的な利用パターンごとにおすすめの選択肢を示します。

メリット/デメリット一覧

#観点type の特徴interface の特徴
1拡張性宣言のマージ不可。合成は交差型 & で明示宣言のマージと extends で段階的に拡張しやすい
2表現力ユニオン・条件型・テンプレートリテラル型など高度な型演算が可能構造宣言に特化。リテラル・条件型など直接記述は不可
3IDE 補完型計算が多いと補完候補が長くなる傾向オブジェクト構造を宣言するため補完が直感的
4ライブラリ互換外部公開では breaking change を誘発しやすい再宣言マージで後方互換を保ちやすい
5学習コスト条件型や infer を理解する必要があるJava Interface に近く初学者に理解しやすい

パターン別おすすめ

#シーン推奨理由
1公開 SDK・型定義ファイルinterfaceユーザー側で拡張できるため将来互換が保たれる
2内部ユーティリティ関数群type条件型・テンプレートリテラル型で汎用型を生成しやすい
3REST API レスポンス DTOinterfaceバージョンごとの追加項目を宣言マージで吸収できる
4Redux Action など列挙的ユニオンtype複数リテラルを `
5ドメイン ID や定数の別名typeプリミティブ別名を簡潔に定義できる
6プラグインアーキテクチャinterface + type外部契約を interface、オプション型を type で表現
7JSX Props 型(拡張前提)interfaceextends で派生コンポーネントに対応しやすい
8再帰ツリーや JSON 型type再帰型と条件型を組み合わせた表現が容易

運用のヒント

  1. ライブラリや共有コードでは「まず interface で契約を定義し、必要に応じて type で派生型を組み立てる」方針が安全です。
  2. アプリ内部ロジックでは type を主役にし、ユニオン・条件型で型安全と可読性を高めると保守が楽になります。
  3. 型選択はファイル単位で統一せず、機能ごとに最適な方を柔軟に混在させると設計が洗練されます。

まとめ

最初に押さえるべき核心は 「拡張性なら interface、型演算なら type」 というシンプルな方針です。
宣言のマージや extends による段階的拡張が必要な公開 API では interface が安全であり、ユニオン・条件型・テンプレートリテラル型など高度な表現力を活かしたい内部ユーティリティでは type が威力を発揮します。

実装面では次の三点を意識すると設計が安定します。

  1. 契約を先に定義し拡張は後から加える
    外部向けの DTO やプラグイン API は interface で宣言し、互換性を維持したまま追加できる形にする。

  2. 型演算は専用のユーティリティ型に集約する
    type の条件型や infer を使い回し、実ロジック側をシンプルに保つことで可読性を高める。

  3. IDE 補完とコンパイルエラーの質を常に評価する
    interface の直感的な補完と type の強力な型ガードを使い分け、開発者体験を最適化する。

最後に、チームルールを「宣言マージ=interface」「型演算=type」と明確に分けるだけでも保守コストは大幅に削減されます。

ぜひ活用してみてください。

関連リンク

TypeScriptの記事Typescript