はじめに

TypeScriptでオブジェクトの型を定義する方法として、type(型エイリアス)とinterfaceの2つがあります。どちらもオブジェクトの構造を定義できますが、それぞれに固有の特徴があり、状況によって使い分けが必要です。

本記事では、TypeScriptのtypeinterfaceの違いを詳しく解説し、ユニオン型・交差型での使い分け、宣言マージの挙動、パフォーマンスの観点、そしてプロジェクトでの命名規則まで網羅的に説明します。

この記事を読み終えると、以下のことができるようになります。

  • typeinterfaceそれぞれの基本構文と特徴を理解できる
  • 拡張方法の違い(extends vs 交差型)を使い分けられる
  • 宣言マージが必要な場面でinterfaceを適切に選択できる
  • ユニオン型やプリミティブ型のエイリアスにはtypeを使うべき理由がわかる
  • プロジェクトのコーディング規約に沿った命名規則を適用できる

実行環境・前提条件

前提知識

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • typeinterfaceの両方でオブジェクト型を定義し、同等に使用できる
  • 拡張方法の違いによるコンパイル結果の差異を確認できる
  • 宣言マージの挙動を理解し、意図した型定義ができる

TypeScriptにおけるtypeとinterfaceの基本

まず、typeinterfaceの基本的な構文を確認しましょう。

graph TB
    A[TypeScript オブジェクト型定義] --> B[interface]
    A --> C[type]
    B --> D["interface User {<br/>  name: string;<br/>  age: number;<br/>}"]
    C --> E["type User = {<br/>  name: string;<br/>  age: number;<br/>}"]

interfaceの基本構文

interfaceはオブジェクトの形状(Shape)を定義するための専用構文です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface User {
  name: string;
  age: number;
  email?: string; // オプショナルプロパティ
}

const user: User = {
  name: "田中太郎",
  age: 30
};

typeの基本構文

type(型エイリアス)は、任意の型に名前を付けるための構文です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type User = {
  name: string;
  age: number;
  email?: string;
};

const user: User = {
  name: "田中太郎",
  age: 30
};

オブジェクト型の定義においては、typeinterfaceは見た目も機能もほぼ同等です。しかし、それぞれに固有の特徴があります。

typeとinterfaceの主要な違い

TypeScriptのtypeinterfaceには、いくつかの重要な違いがあります。以下の表で概要を把握しましょう。

機能 interface type
オブジェクト型の定義 可能 可能
拡張方法 extendsキーワード 交差型(&
宣言マージ 可能 不可
ユニオン型の定義 不可 可能
プリミティブ型のエイリアス 不可 可能
タプル型の定義 不可 可能
implementsでの使用 可能 可能(オブジェクト型の場合)
コンパイラパフォーマンス 優れている やや劣る(複雑な交差型の場合)

それぞれの違いについて、詳しく見ていきましょう。

型の拡張方法の違い

interfacetypeでは、既存の型を拡張する方法が異なります。

interfaceのextends

interfaceextendsキーワードを使って他のinterfaceを継承できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

const dog: Dog = {
  name: "ポチ",
  breed: "柴犬",
  bark() {
    console.log("ワンワン!");
  }
};

複数のinterfaceを同時に継承することも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

interface Duck extends Animal, Flyable, Swimmable {
  quack(): void;
}

const duck: Duck = {
  name: "ドナルド",
  fly() {
    console.log("飛んでいます");
  },
  swim() {
    console.log("泳いでいます");
  },
  quack() {
    console.log("ガーガー!");
  }
};

typeの交差型(Intersection Types)

typeは交差型(&)を使って複数の型を組み合わせます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Animal = {
  name: string;
};

type Dog = Animal & {
  breed: string;
  bark(): void;
};

const dog: Dog = {
  name: "ポチ",
  breed: "柴犬",
  bark() {
    console.log("ワンワン!");
  }
};

同様に、複数の型を組み合わせることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Flyable = {
  fly(): void;
};

type Swimmable = {
  swim(): void;
};

type Duck = Animal & Flyable & Swimmable & {
  quack(): void;
};

extendsと交差型の違い

一見同じように見えるextendsと交差型ですが、プロパティの競合時の挙動が異なります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// interfaceの場合:競合するとコンパイルエラー
interface Base {
  id: number;
}

// エラー: Interface 'Extended' incorrectly extends interface 'Base'.
// interface Extended extends Base {
//   id: string; // 型が競合
// }

// typeの場合:交差型は両方の条件を満たす型になる
type BaseType = {
  id: number;
};

type ExtendedType = BaseType & {
  id: string;
};

// ExtendedType の id は number & string = never となる

interfaceextendsは、競合を早期にエラーとして検出できるため、意図しない型の競合を防げます。

宣言マージ(Declaration Merging)

interfaceの最大の特徴の一つが「宣言マージ」です。同じ名前のinterfaceを複数回宣言すると、自動的にマージされます。

interfaceの宣言マージ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface User {
  name: string;
}

interface User {
  age: number;
}

interface User {
  email?: string;
}

// 上記3つの宣言がマージされ、以下と同等になる
// interface User {
//   name: string;
//   age: number;
//   email?: string;
// }

const user: User = {
  name: "田中太郎",
  age: 30
  // emailは省略可能
};

typeでは宣言マージができない

typeは同じ名前で再宣言するとコンパイルエラーになります。

1
2
3
4
5
6
7
8
type User = {
  name: string;
};

// エラー: Duplicate identifier 'User'.
// type User = {
//   age: number;
// };

宣言マージの実用的なユースケース

宣言マージは、以下のような場面で活用されます。

1. ライブラリの型拡張

外部ライブラリの型を拡張する際に宣言マージが役立ちます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Express.jsのRequest型を拡張する例
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        name: string;
      };
    }
  }
}

// これにより、req.user にアクセスできるようになる

2. グローバル型の拡張

Window オブジェクトに独自のプロパティを追加する例です。

1
2
3
4
5
6
7
8
9
interface Window {
  myApp: {
    version: string;
    init(): void;
  };
}

// TypeScriptがwindow.myAppを認識するようになる
window.myApp.init();

3. モジュール型の拡張

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 元のモジュール定義
declare module "some-library" {
  interface Config {
    apiKey: string;
  }
}

// 別ファイルで拡張
declare module "some-library" {
  interface Config {
    debug?: boolean;
  }
}

ユニオン型とtypeの関係

typeの最大の強みは、ユニオン型を定義できることです。interfaceではこれができません。

ユニオン型の定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// typeでユニオン型を定義
type Status = "pending" | "approved" | "rejected";

type Result = 
  | { success: true; data: string }
  | { success: false; error: Error };

// 使用例
function handleResult(result: Result) {
  if (result.success) {
    console.log(result.data);
  } else {
    console.error(result.error.message);
  }
}

Discriminated Unions(判別可能なユニオン型)

TypeScriptで頻繁に使用されるパターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

interfaceでユニオン型を表現する場合の制限

interface単体ではユニオン型を定義できませんが、typeと組み合わせることは可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface Circle {
  kind: "circle";
  radius: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

// interfaceをtypeでユニオンにまとめる
type Shape = Circle | Rectangle;

プリミティブ型・タプル型のエイリアス

typeはプリミティブ型やタプル型にもエイリアスを付けられます。interfaceにはできないことです。

プリミティブ型のエイリアス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// プリミティブ型にセマンティックな名前を付ける
type UserId = string;
type Age = number;
type IsActive = boolean;

// 関数シグネチャの可読性が向上
function getUser(id: UserId): { name: string; age: Age } {
  // 実装
  return { name: "田中太郎", age: 30 };
}

リテラル型のユニオン

1
2
3
4
5
6
7
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;

function makeRequest(method: HttpMethod, url: string): Promise<StatusCode> {
  // 実装
  return Promise.resolve(200);
}

タプル型の定義

1
2
3
4
5
6
7
type Coordinate = [number, number];
type RGB = [number, number, number];
type NameAgePair = [string, number];

const point: Coordinate = [10, 20];
const color: RGB = [255, 128, 0];
const person: NameAgePair = ["田中太郎", 30];

複雑なタプル型

1
2
3
4
5
6
7
8
// 可変長タプル
type StringNumberBooleans = [string, number, ...boolean[]];

// ラベル付きタプル(TypeScript 4.0以降)
type Point3D = [x: number, y: number, z: number];

// readonly タプル
type ReadonlyCoordinate = readonly [number, number];

コンパイラパフォーマンスの違い

TypeScriptの公式ドキュメントでは、interfaceextendsを使用した方が、typeの交差型よりもコンパイラのパフォーマンスが優れていると述べられています。

graph LR
    A[型定義] --> B{複雑な型?}
    B -->|Yes| C[interface + extends<br/>推奨]
    B -->|No| D[どちらでも可]
    C --> E[キャッシュされやすい]
    D --> F[type or interface]

なぜinterfaceの方がパフォーマンスが良いのか

interfaceは名前付きの型として扱われ、TypeScriptコンパイラ内部でキャッシュされやすい構造になっています。一方、交差型は評価のたびに計算が必要になる場合があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// パフォーマンスの観点から推奨
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// 複雑なプロジェクトでは避けた方が良い場合もある
type AnimalType = {
  name: string;
};

type DogType = AnimalType & {
  breed: string;
};

ただし、通常の規模のプロジェクトでは、この差は体感できないほど小さいです。パフォーマンスを理由に選択を変える必要があるのは、非常に大規模なプロジェクトの場合のみです。

クラスでの使用(implements)

interfacetypeの両方をクラスでimplementsできます。

interfaceをimplementsする

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
interface Printable {
  print(): void;
}

interface Serializable {
  serialize(): string;
}

class Document implements Printable, Serializable {
  constructor(private content: string) {}

  print(): void {
    console.log(this.content);
  }

  serialize(): string {
    return JSON.stringify({ content: this.content });
  }
}

typeをimplementsする

オブジェクト型のtypeであれば、同様にimplementsできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type Printable = {
  print(): void;
};

type Serializable = {
  serialize(): string;
};

class Document implements Printable, Serializable {
  constructor(private content: string) {}

  print(): void {
    console.log(this.content);
  }

  serialize(): string {
    return JSON.stringify({ content: this.content });
  }
}

ただし、ユニオン型やプリミティブ型のエイリアスはimplementsできません。

1
2
3
4
type Status = "active" | "inactive";

// エラー: A class can only implement an object type or intersection of object types with statically known members.
// class StatusClass implements Status {}

プロジェクトでの命名規則

TypeScriptプロジェクトでは、typeinterfaceの命名規則を統一することが重要です。

Microsoftの推奨ガイドライン

Microsoftの公式ガイドラインでは、以下の命名規則が推奨されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 推奨:プレフィックスなし
interface User {
  name: string;
}

type UserStatus = "active" | "inactive";

// 非推奨:Iプレフィックス(C#スタイル)
// interface IUser {
//   name: string;
// }

// 非推奨:Typeサフィックス
// type UserType = { name: string };

実践的な命名規則

プロジェクトで統一すべき命名規則の例を紹介します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// エンティティ・モデル
interface User { ... }
interface Product { ... }
interface Order { ... }

// Props(React)
interface ButtonProps { ... }
interface ModalProps { ... }

// ユニオン型(状態、ステータス)
type UserRole = "admin" | "user" | "guest";
type LoadingState = "idle" | "loading" | "success" | "error";

// 関数型
type EventHandler = (event: Event) => void;
type AsyncFunction<T> = () => Promise<T>;

// ユーティリティ型(プロジェクト固有)
type Nullable<T> = T | null;
type Optional<T> = T | undefined;

使い分けの判断フローチャート

以下のフローチャートを参考に、typeinterfaceを使い分けましょう。

flowchart TD
    A[型を定義したい] --> B{どのような型?}
    B -->|オブジェクト型| C{宣言マージが必要?}
    B -->|ユニオン型| D[type を使用]
    B -->|タプル型| D
    B -->|プリミティブのエイリアス| D
    B -->|関数型| D
    C -->|Yes| E[interface を使用]
    C -->|No| F{拡張が必要?}
    F -->|Yes| G{extends を使いたい?}
    F -->|No| H[どちらでも可<br/>プロジェクト規約に従う]
    G -->|Yes| E
    G -->|No| I[type + 交差型]

使い分けのまとめ

interfaceを使うべき場面

  • オブジェクトの形状を定義する(基本的なケース)
  • 宣言マージが必要な場合(ライブラリの型拡張など)
  • extendsによる明確な継承関係を表現したい場合
  • 大規模プロジェクトでコンパイラパフォーマンスを重視する場合

typeを使うべき場面

  • ユニオン型を定義する場合
  • タプル型を定義する場合
  • プリミティブ型にセマンティックな名前を付ける場合
  • 関数型を定義する場合
  • 複雑な型の組み合わせ(交差型、条件付き型など)

実践的なコード例

実際のプロジェクトでの使い分け例を見てみましょう。

API レスポンスの型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 基本的なエンティティはinterface
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  publishedAt: Date | null;
}

// APIレスポンスのラッパーはtype(ジェネリクスとの組み合わせ)
type ApiResponse<T> = 
  | { status: "success"; data: T }
  | { status: "error"; error: { code: string; message: string } };

// 使用例
type UserResponse = ApiResponse<User>;
type PostListResponse = ApiResponse<Post[]>;

async function fetchUser(id: string): Promise<UserResponse> {
  try {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();
    return { status: "success", data };
  } catch (e) {
    return { 
      status: "error", 
      error: { code: "FETCH_ERROR", message: "Failed to fetch user" } 
    };
  }
}

Reactコンポーネントの型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Propsはinterfaceで定義
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
  children?: React.ReactNode;
}

// イベントハンドラーの型はtype
type ClickHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;

// コンポーネントの状態のユニオン型はtype
type ButtonState = "idle" | "loading" | "success" | "error";

// 複合的な型はtypeで組み合わせ
type ButtonPropsWithState = ButtonProps & {
  state: ButtonState;
};

状態管理の型定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Reduxスタイルの状態管理
interface UserState {
  currentUser: User | null;
  isLoading: boolean;
  error: string | null;
}

// アクション型はtypeでユニオン
type UserAction =
  | { type: "LOGIN_START" }
  | { type: "LOGIN_SUCCESS"; payload: User }
  | { type: "LOGIN_FAILURE"; payload: string }
  | { type: "LOGOUT" };

// Reducerの型定義
type UserReducer = (state: UserState, action: UserAction) => UserState;

const userReducer: UserReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN_START":
      return { ...state, isLoading: true, error: null };
    case "LOGIN_SUCCESS":
      return { ...state, isLoading: false, currentUser: action.payload };
    case "LOGIN_FAILURE":
      return { ...state, isLoading: false, error: action.payload };
    case "LOGOUT":
      return { ...state, currentUser: null };
    default:
      return state;
  }
};

まとめ

TypeScriptのtypeinterfaceは、多くの場面で互換的に使用できますが、それぞれに固有の特徴があります。

本記事で解説した主要なポイントを振り返りましょう。

  • オブジェクト型の定義typeinterfaceの両方で可能
  • 拡張方法interfaceextendstypeは交差型(&
  • 宣言マージinterfaceのみ可能(ライブラリ拡張に有用)
  • ユニオン型・タプル型typeのみで定義可能
  • コンパイラパフォーマンス:大規模プロジェクトではinterfaceが有利
  • 命名規則:プレフィックス(Iなど)は使わないのが現代の推奨

プロジェクトでは、チーム内で一貫した規約を設け、どちらを使うかを明確にすることが重要です。TypeScript公式ドキュメントでは「迷ったらinterfaceを使い、typeの機能が必要になったらtypeを使う」というヒューリスティックが提案されています。

次回は、ユニオン型とリテラル型について、より詳しく解説していきます。

参考リンク