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 で実装する際は、静的型付けとインターフェースを活かすことで、型安全で保守性の高いコードが実現できます。ただし、すべてのパターンを常に適用すべきではありません。
- 生成パターンは、ジェネリクスやユーティリティ型と組み合わせることで型安全性が向上する
- 構造パターンは、インターフェースによる契約の明確化が鍵となる
- 振る舞いパターンは、関数型プログラミングとの併用でより簡潔になる場合がある
- パターン選択は「課題の構造」を見極めてから行うべきであり、パターンありきで考えない
設計パターンは解決策のカタログです。まず問題を明確にし、その問題に適したパターンを選択することが、堅牢なソフトウェア設計への第一歩となります。
関連リンク
著書
article2026年1月23日TypeScriptのtypeとinterfaceを比較・検証する 違いと使い分けの判断基準を整理
article2026年1月23日TypeScript 5.8の型推論を比較・検証する 強化点と落とし穴の回避策
article2026年1月23日TypeScript Genericsの使用例を早見表でまとめる 記法と頻出パターンを整理
article2026年1月22日TypeScriptの型システムを概要で理解する 基礎から全体像まで完全解説
article2026年1月22日ZustandとTypeScriptのユースケース ストアを型安全に設計して運用する実践
article2026年1月22日TypeScriptでよく出るエラーをトラブルシュートでまとめる 原因と解決法30選
articleshadcn/ui × TanStack Table 設計術:仮想化・列リサイズ・アクセシブルなグリッド
articleRemix のデータ境界設計:Loader・Action とクライアントコードの責務分離
articlePreact コンポーネント設計 7 原則:再レンダリング最小化の分割と型付け
articlePHP 8.3 の新機能まとめ:readonly クラス・型強化・性能改善を一気に理解
articleNotebookLM に PDF/Google ドキュメント/URL を取り込む手順と最適化
articlePlaywright 並列実行設計:shard/grep/fixtures で高速化するテストスイート設計術
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
