T-CREATOR

<div />

TypeScriptでGOF設計パターンを概要で学ぶ 実装ガイドとして使える形に整理

2026年1月20日
TypeScriptでGOF設計パターンを概要で学ぶ 実装ガイドとして使える形に整理

TypeScript で GoF 設計パターンを実装する際、どのパターンをどの場面で使うべきか迷うことはありませんか。静的型付けとインターフェースを活かした型安全な実装を行うには、各パターンの特性と落とし穴を理解しておく必要があります。この記事では、GoF 23 パターンを 3 つのカテゴリに分類し、TypeScript での実装ガイドとして使いどころと注意点を整理します。

GoF 設計パターン 3 カテゴリの比較

カテゴリ主な目的代表パターンTypeScript との相性
生成パターンオブジェクト生成の柔軟化Singleton, Factory Method, Builderジェネリクス・コンストラクタ型で型安全に実装可能
構造パターンクラス・オブジェクト構成の整理Adapter, Decorator, Facadeインターフェースで契約を明確化できる
振る舞いパターンオブジェクト間の責務分担Strategy, Observer, State関数型との組み合わせで簡潔に表現可能

それぞれのカテゴリには異なる設計意図があり、TypeScript の静的型付けを活かすことで、従来の JavaScript 実装より堅牢なコードが書けます。以下で詳細を解説します。

検証環境

  • OS: macOS Sequoia 15.2
  • Node.js: 22.13.0
  • TypeScript: 5.7.3
  • 主要パッケージ:
    • tsx: 4.19.2
  • 検証日: 2026 年 1 月 20 日

GoF 設計パターンが TypeScript 開発で重要な理由

ここでは、なぜ静的型付け言語で設計パターンを学ぶ意義があるのかを整理します。

JavaScript では動的型付けゆえにパターンの意図が曖昧になりがちでした。TypeScript の登場により、インターフェースや型エイリアスで「契約」を明示できるようになり、設計パターンの本来の目的である「再利用可能で保守性の高いコード」が実現しやすくなりました。

つまずきやすい点:設計パターンを「暗記するもの」と捉えると、不適切な場面で無理に適用してしまう。問題の構造を見極めてからパターンを選ぶ姿勢が重要です。

実際に業務で設計パターンを導入した際、最初は「パターンありき」で考えてしまい、かえってコードが複雑化した経験があります。パターンは解決策のカタログであり、まず課題を明確にすることが先決です。

mermaidflowchart LR
  problem["課題の特定"] --> select["パターン選択"]
  select --> implement["TypeScript で実装"]
  implement --> verify["型チェックで検証"]

上図は、設計パターン適用の基本フローを示しています。課題を特定してからパターンを選び、TypeScript の型システムで実装の正しさを検証するという流れです。

生成パターン:オブジェクト生成の型安全な制御

生成パターンは、オブジェクトの生成方法を柔軟にするためのパターン群です。TypeScript では、ジェネリクスやコンストラクタ型を活用することで、型安全な実装が可能になります。

Singleton パターンと TypeScript のモジュールシステム

Singleton(シングルトン)は、クラスのインスタンスが 1 つだけであることを保証するパターンです。

TypeScript では、ES Modules のモジュールスコープを利用することで、クラスを使わずに Singleton を実現できます。検証の結果、モジュールレベルでの実装が最もシンプルで保守性が高いことがわかりました。

typescript// configService.ts - モジュールスコープによる Singleton
interface Config {
  apiUrl: string;
  timeout: number;
}

const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

export const getConfig = (): Readonly<Config> => config;
export const updateConfig = (updates: Partial<Config>): void => {
  Object.assign(config, updates);
};

落とし穴:クラスベースの Singleton でプライベートコンストラクタを使う方法もありますが、テスト時にモックしづらくなります。DI(依存性注入)との併用を検討してください。

Factory Method パターンとジェネリクスの活用

Factory Method(ファクトリーメソッド)は、オブジェクト生成をサブクラスに委譲するパターンです。TypeScript ではジェネリクスを組み合わせることで、生成する型を柔軟に指定できます。

typescript// 型安全な Factory Method
interface Product {
  name: string;
  operation(): string;
}

class ConcreteProductA implements Product {
  name = "ProductA";
  operation(): string {
    return `${this.name} の処理結果`;
  }
}

class ConcreteProductB implements Product {
  name = "ProductB";
  operation(): string {
    return `${this.name} の処理結果`;
  }
}

// コンストラクタ型を使った型安全なファクトリ関数
function createProduct<T extends Product>(ProductClass: new () => T): T {
  return new ProductClass();
}

const productA = createProduct(ConcreteProductA);
const productB = createProduct(ConcreteProductB);

実際に試したところ、new () => T というコンストラクタ型を使うことで、存在しないクラスを渡した場合にコンパイルエラーになり、実行時エラーを防げました。

Builder パターンと Partial 型の連携

Builder(ビルダー)は、複雑なオブジェクトを段階的に構築するパターンです。TypeScript の Partial<T> ユーティリティ型と組み合わせることで、構築途中の不完全な状態を型安全に扱えます。

typescriptinterface UserProfile {
  id: number;
  username: string;
  email: string;
  bio?: string;
}

class UserProfileBuilder {
  private profile: Partial<UserProfile> = {};

  setId(id: number): this {
    this.profile.id = id;
    return this;
  }

  setUsername(username: string): this {
    this.profile.username = username;
    return this;
  }

  setEmail(email: string): this {
    this.profile.email = email;
    return this;
  }

  setBio(bio: string): this {
    this.profile.bio = bio;
    return this;
  }

  build(): UserProfile {
    if (
      this.profile.id === undefined ||
      !this.profile.username ||
      !this.profile.email
    ) {
      throw new Error("id, username, email は必須です");
    }
    return this.profile as UserProfile;
  }
}

注意点build() での型アサーション(as UserProfile)は避けたい場合、branded types や型ガードを使う方法もあります。

構造パターン:インターフェースによる契約の明確化

構造パターンは、クラスやオブジェクトの構成を整理するためのパターン群です。TypeScript のインターフェースを使うことで、契約(コントラクト)を明確に定義できます。

Adapter パターンと既存 API の統合

Adapter(アダプター)は、互換性のないインターフェースを変換するパターンです。外部 API やレガシーコードとの統合で頻繁に使用します。

mermaidflowchart LR
  client["クライアント"] --> adapter["Adapter"]
  adapter --> legacy["レガシー API"]
  adapter --> external["外部 API"]

上図は、Adapter パターンの構造を示しています。クライアントは統一されたインターフェースを通じて、異なる API にアクセスできます。

typescript// 外部 API の型定義(変更不可)
interface LegacyUser {
  user_name: string;
  user_email: string;
}

// アプリケーション内部で使う型
interface User {
  name: string;
  email: string;
}

// Adapter クラス
class UserAdapter {
  static fromLegacy(legacy: LegacyUser): User {
    return {
      name: legacy.user_name,
      email: legacy.user_email,
    };
  }

  static toLegacy(user: User): LegacyUser {
    return {
      user_name: user.name,
      user_email: user.email,
    };
  }
}

業務で問題になったケースとして、Adapter 内でデータ変換のロジックが肥大化し、本来の変換責務を超えてしまったことがあります。Adapter は「変換」に徹し、ビジネスロジックは含めないようにしましょう。

Decorator パターンと機能の動的追加

Decorator(デコレータ)は、オブジェクトに動的に責務を追加するパターンです。GoF の Decorator パターンと、TypeScript のデコレータ構文は別物である点に注意してください。

typescript// インターフェースで契約を定義
interface Coffee {
  getCost(): number;
  getDescription(): string;
}

class SimpleCoffee implements Coffee {
  getCost(): number {
    return 300;
  }
  getDescription(): string {
    return "シンプルコーヒー";
  }
}

// 抽象デコレータ
abstract class CoffeeDecorator implements Coffee {
  constructor(protected coffee: Coffee) {}
  abstract getCost(): number;
  abstract getDescription(): string;
}

class MilkDecorator extends CoffeeDecorator {
  getCost(): number {
    return this.coffee.getCost() + 50;
  }
  getDescription(): string {
    return `${this.coffee.getDescription()}, ミルク追加`;
  }
}

// 使用例
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
console.log(`${coffee.getDescription()}: ${coffee.getCost()}円`);

TypeScript デコレータ構文との違い:TypeScript の @decorator 構文はクラスやメソッドに対するメタプログラミング機能であり、GoF の Decorator パターンとは異なる概念です。混同しないようにしましょう。

Facade パターンと複雑なサブシステムの隠蔽

Facade(ファサード)は、複雑なサブシステムへのシンプルなインターフェースを提供するパターンです。

typescript// 複雑なサブシステム
class VideoDecoder {
  decode(file: string): string {
    return `${file} をデコード`;
  }
}

class AudioDecoder {
  decode(file: string): string {
    return `${file} の音声をデコード`;
  }
}

class SubtitleLoader {
  load(file: string): string {
    return `${file} の字幕を読み込み`;
  }
}

// Facade で統一インターフェースを提供
class MediaPlayerFacade {
  private videoDecoder = new VideoDecoder();
  private audioDecoder = new AudioDecoder();
  private subtitleLoader = new SubtitleLoader();

  play(file: string): void {
    console.log(this.videoDecoder.decode(file));
    console.log(this.audioDecoder.decode(file));
    console.log(this.subtitleLoader.load(file));
    console.log("再生開始");
  }
}

振る舞いパターン:責務分担と関数型アプローチ

振る舞いパターンは、オブジェクト間の責務分担やアルゴリズムのカプセル化を扱います。TypeScript では関数型プログラミングの要素と組み合わせることで、より簡潔な実装が可能です。

Strategy パターンと関数による戦略の切り替え

Strategy(ストラテジー)は、アルゴリズムをカプセル化して交換可能にするパターンです。TypeScript ではインターフェースだけでなく、関数型で実装することもできます。

typescript// クラスベースの Strategy
interface SortStrategy {
  sort(data: number[]): number[];
}

class BubbleSort implements SortStrategy {
  sort(data: number[]): number[] {
    const arr = [...data];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}

// 関数型の Strategy(より簡潔)
type SortFn = (data: number[]) => number[];

const quickSort: SortFn = (data) => [...data].sort((a, b) => a - b);
const reverseSort: SortFn = (data) => [...data].sort((a, b) => b - a);

class Sorter {
  constructor(private strategy: SortFn) {}

  setStrategy(strategy: SortFn): void {
    this.strategy = strategy;
  }

  execute(data: number[]): number[] {
    return this.strategy(data);
  }
}

検証の結果、単純なアルゴリズムの切り替えであれば関数型の方が簡潔でした。ただし、戦略が状態を持つ必要がある場合はクラスベースが適しています。

Observer パターンとイベント駆動設計

Observer(オブザーバー)は、オブジェクトの状態変化を他のオブジェクトに通知するパターンです。イベント駆動型のアプリケーションで広く使われます。

mermaidflowchart TB
  subject["Subject<br/>(状態保持)"] --> observer1["Observer A"]
  subject --> observer2["Observer B"]
  subject --> observer3["Observer C"]

上図は、Observer パターンの通知の流れを示しています。Subject の状態が変化すると、登録されたすべての Observer に通知されます。

typescript// 型安全な Observer パターン
interface Observer<T> {
  update(data: T): void;
}

class Subject<T> {
  private observers: Observer<T>[] = [];

  subscribe(observer: Observer<T>): void {
    this.observers.push(observer);
  }

  unsubscribe(observer: Observer<T>): void {
    this.observers = this.observers.filter((o) => o !== observer);
  }

  notify(data: T): void {
    this.observers.forEach((observer) => observer.update(data));
  }
}

// 使用例
interface StockPrice {
  symbol: string;
  price: number;
}

const stockSubject = new Subject<StockPrice>();

const priceDisplay: Observer<StockPrice> = {
  update: (data) => console.log(`${data.symbol}: ${data.price}円`),
};

stockSubject.subscribe(priceDisplay);
stockSubject.notify({ symbol: "AAPL", price: 15000 });

落とし穴:Observer の登録解除を忘れるとメモリリークの原因になります。コンポーネントのライフサイクルに合わせた解除処理を必ず実装してください。

State パターンと状態遷移の型安全な管理

State(ステート)は、オブジェクトの内部状態に応じて振る舞いを変えるパターンです。有限状態マシン(FSM)の実装に適しています。

typescript// 型安全な State パターン
interface DocumentState {
  publish(doc: Document): void;
  getName(): string;
}

class DraftState implements DocumentState {
  publish(doc: Document): void {
    console.log("レビュー依頼を送信");
    doc.setState(new ReviewState());
  }
  getName(): string {
    return "下書き";
  }
}

class ReviewState implements DocumentState {
  publish(doc: Document): void {
    console.log("公開処理を実行");
    doc.setState(new PublishedState());
  }
  getName(): string {
    return "レビュー中";
  }
}

class PublishedState implements DocumentState {
  publish(doc: Document): void {
    console.log("すでに公開済みです");
  }
  getName(): string {
    return "公開済み";
  }
}

class Document {
  private state: DocumentState = new DraftState();

  setState(state: DocumentState): void {
    this.state = state;
  }

  publish(): void {
    this.state.publish(this);
  }

  getStateName(): string {
    return this.state.getName();
  }
}

フロントエンド・バックエンドでの実践的な適用

設計パターンは、フレームワークやライブラリの設計にも組み込まれています。

React/Vue での設計パターンの現れ方

パターンフレームワークでの現れ方
Compositeコンポーネントツリーの構造そのもの
Observer状態変更による再レンダリング
Strategyカスタムフック・Composables による処理の差し替え
Factoryコンポーネントの動的生成

NestJS と依存性注入

NestJS のようなバックエンドフレームワークでは、DI(Dependency Injection)が標準で組み込まれています。これは Factory パターンと Singleton パターンの組み合わせで実現されています。

typescript// NestJS での DI 例(概念的なコード)
@Injectable()
class UserService {
  constructor(private readonly userRepository: UserRepository) {}

  findAll(): User[] {
    return this.userRepository.findAll();
  }
}

設計パターン選択の判断基準(詳細比較)

ここまでの内容を踏まえ、どのパターンをどの場面で使うべきかを整理します。

パターン適している場面避けるべき場面TypeScript の活用ポイント
Singleton設定情報、ロガーなどテスト容易性が重要な場合モジュールスコープ、Readonly<T>
Factory Method生成ロジックの隠蔽が必要生成パターンが単純な場合コンストラクタ型、ジェネリクス
Builder複雑なオブジェクト構築プロパティが少ない場合Partial<T>、メソッドチェーン
Adapter外部 API との統合新規開発で互換性不要インターフェース変換
Decorator機能の動的追加単純な継承で十分な場合抽象クラス、インターフェース
Strategyアルゴリズムの切り替え戦略が 1 つしかない場合関数型、ジェネリクス
Observerイベント駆動設計同期処理で十分な場合ジェネリクス、型安全なコールバック
State状態遷移の明確化状態が 2 つ以下の場合Union 型、判別共用体

実務での経験則:パターンを適用するかどうかの判断は、「将来の変更可能性」がポイントです。変更が予想されない箇所に過度なパターンを適用すると、かえって複雑性が増します。

まとめ

GoF 設計パターンを TypeScript で実装する際は、静的型付けとインターフェースを活かすことで、型安全で保守性の高いコードが実現できます。ただし、すべてのパターンを常に適用すべきではありません。

  • 生成パターンは、ジェネリクスやユーティリティ型と組み合わせることで型安全性が向上する
  • 構造パターンは、インターフェースによる契約の明確化が鍵となる
  • 振る舞いパターンは、関数型プログラミングとの併用でより簡潔になる場合がある
  • パターン選択は「課題の構造」を見極めてから行うべきであり、パターンありきで考えない

設計パターンは解決策のカタログです。まず問題を明確にし、その問題に適したパターンを選択することが、堅牢なソフトウェア設計への第一歩となります。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;