【TypeScript】デザインパターン完全実装ガイド|Singleton・Factory・Builder・Strategy・Observer・Command・State を型安全に実装する

デザインパターンは「設計の共通語」であり、チーム開発での意思疎通を大幅に効率化します。TypeScript でパターンを実装すると、型システムがパターンの制約を自動的に強制するため、実装ミスをコンパイル時に検出できます。

n

この記事では GoF デザインパターンから厳選した 7 つを、TypeScript の型機能を最大限活用した実践的な実装で解説します。「なぜそのパターンを使うか」「TypeScript では何が変わるか」を意識しながら読んでください。

n

この記事で実装するパターン

  • Singleton — 型安全なシングルトン(ジェネリクス版)
  • Factory Method — 条件分岐のない型安全な生成ロジック
  • Abstract Factory — 関連オブジェクト群を一括生成
  • Builder — fluent API による型安全なオブジェクト構築
  • Strategy — アルゴリズムの差し替えを型で保証
  • Observer(TypedEventEmitter) — イベント名と型を紐づけた型安全な監視
  • Command — 操作の記録・Undo を型安全に実装
  • State Machine — Discriminated Union による型安全な状態遷移

n

n

n

n

n

n

n

n

n

n

n

n

n

分類 パターン 解決する問題
生成(Creational) Singleton インスタンスが 1 つだけであることを保証
生成(Creational) Factory Method 生成ロジックをサブクラス/関数に委譲
生成(Creational) Abstract Factory 関連オブジェクト群をまとめて生成
生成(Creational) Builder 複雑なオブジェクトを段階的に構築
振る舞い(Behavioral) Strategy アルゴリズムをオブジェクトとして差し替え可能にする
振る舞い(Behavioral) Observer オブジェクトの状態変化を複数の監視者に通知
振る舞い(Behavioral) Command 操作をオブジェクト化して記録・取り消しを可能にする
振る舞い(Behavioral) State Machine 状態と遷移を型で完全に定義し、無効な遷移を防ぐ

n

スポンサーリンク

Singleton パターン

n

Singleton はアプリケーション全体でインスタンスを 1 つだけ保持したいとき(DB 接続・設定管理・ロガーなど)に使います。TypeScript では プライベートコンストラクタ + static メソッドで実装できます。

n

// ─── 基本実装 ───nclass AppConfig {n  private static instance: AppConfig | null = null;nn  private constructor(n    public readonly apiUrl:    string,n    public readonly maxRetry:  number,n    public readonly debug:     booleann  ) {}nn  static getInstance(): AppConfig {n    if (AppConfig.instance === null) {n      AppConfig.instance = new AppConfig(n        process.env.API_URL ?? "https://api.example.com",n        Number(process.env.MAX_RETRY ?? 3),n        process.env.NODE_ENV === "development"n      );n    }n    return AppConfig.instance;n  }nn  // テスト用にリセットできるようにするn  static reset(): void {n    AppConfig.instance = null;n  }n}nn// 利用例nconst config1 = AppConfig.getInstance();nconst config2 = AppConfig.getInstance();nconsole.log(config1 === config2); // true(同じインスタンス)nn// new AppConfig(...) → コンパイルエラー(private constructor)n

n

ジェネリクス版 Singleton(汎用ファクトリー)

n

// ─── どんなクラスでも Singleton にできる汎用実装 ───ntype Constructor<T> = new (...args: unknown[]) => T;nnclass SingletonRegistry {n  private static instances = new Map<Constructor<unknown>, unknown>();nn  static get<T>(Ctor: Constructor<T>): T {n    if (!SingletonRegistry.instances.has(Ctor)) {n      SingletonRegistry.instances.set(Ctor, new Ctor());n    }n    return SingletonRegistry.instances.get(Ctor) as T;n  }nn  static reset<T>(Ctor: Constructor<T>): void {n    SingletonRegistry.instances.delete(Ctor);n  }n}nn// 利用例nclass Logger { log(msg: string) { console.log(`[LOG] ${msg}`); } }nclass Database { query(sql: string) { /* ... */ } }nnconst logger = SingletonRegistry.get(Logger);   // Logger 型nconst db     = SingletonRegistry.get(Database); // Database 型n

n

Singleton とテスト容易性
Singleton はグローバル状態を持つため、テストの独立性を損なうことがあります。本番コードでは Singleton を直接参照せず、依存注入(DI: Dependency Injection)パターンと組み合わせてテスト時にモックを差し込めるよう設計するのがベストプラクティスです。テスト用の reset() メソッドを用意するのも有効です。

n

Factory Method パターン

n

Factory Method は「オブジェクトの生成をサブクラスや専用関数に委譲する」パターンです。TypeScript では Discriminated Union + 型安全なマップif/elseswitch なしに実装できます。

n

// ─── 通知サービスの Factory Method ───nn// 各通知チャネルが実装するインターフェースninterface NotificationService {n  send(to: string, message: string): Promise<void>;n  readonly channel: NotificationChannel;n}nntype NotificationChannel = "email" | "sms" | "push" | "slack";nn// 具体的な実装クラスnclass EmailNotification implements NotificationService {n  readonly channel = "email" as const;n  async send(to: string, message: string): Promise<void> {n    console.log(`Email → ${to}: ${message}`);n  }n}nnclass SmsNotification implements NotificationService {n  readonly channel = "sms" as const;n  async send(to: string, message: string): Promise<void> {n    console.log(`SMS → ${to}: ${message}`);n  }n}nnclass SlackNotification implements NotificationService {n  readonly channel = "slack" as const;n  constructor(private readonly webhookUrl: string) {}n  async send(to: string, message: string): Promise<void> {n    console.log(`Slack → #${to}: ${message}`);n  }n}nn// ─── Factory: switch なしに型安全な生成 ───ntype NotificationConfig =n  | { channel: "email" }n  | { channel: "sms" }n  | { channel: "slack"; webhookUrl: string };nnfunction createNotificationService(n  config: NotificationConfign): NotificationService {n  switch (config.channel) {n    case "email": return new EmailNotification();n    case "sms":   return new SmsNotification();n    case "slack": return new SlackNotification(config.webhookUrl);n  }n}nn// ─── TypeScript が網羅性をチェック ───n// NotificationChannel に "push" を追加した場合、n// switch に case "push" が欠けているとコンパイルエラーnn// 利用例nconst emailSvc = createNotificationService({ channel: "email" });nconst slackSvc = createNotificationService({n  channel: "slack",n  webhookUrl: "https://hooks.slack.com/...",n});nawait emailSvc.send("user@example.com", "登録完了しました");n

n

Abstract Factory パターン

n

Abstract Factory は「関連するオブジェクト群をまとめて生成する」パターンです。TypeScript では インターフェースで Factory の「契約」を定義し、実装を差し替えられる設計にします。

n

// ─── UI コンポーネントの Abstract Factory ───n// (Web用・Native用・Test用の3種類を切り替える例)nn// 各コンポーネントのインターフェースninterface Button   { render(): string; onClick(handler: () => void): void; }ninterface TextInput { render(): string; getValue(): string; }ninterface Dialog    { show(title: string, message: string): void; }nn// ─── Abstract Factory インターフェース ───ninterface UIFactory {n  createButton():    Button;n  createTextInput(): TextInput;n  createDialog():    Dialog;n}nn// ─── Web 向け実装 ───nclass WebButton implements Button {n  render() { return "<button>Click</button>"; }n  onClick(handler: () => void) { /* addEventListener */ handler; }n}nclass WebTextInput implements TextInput {n  render() { return "<input type="text">" ; }n  getValue() { return ""; /* document.querySelector... */ }n}nclass WebDialog implements Dialog {n  show(title: string, message: string) { alert(`${title}: ${message}`); }n}nnclass WebUIFactory implements UIFactory {n  createButton()    { return new WebButton(); }n  createTextInput() { return new WebTextInput(); }n  createDialog()    { return new WebDialog(); }n}nn// ─── Test 向け実装(モック)───nclass MockButton implements Button {n  rendered = false;n  clickHandlers: Array<() => void> = [];n  render() { this.rendered = true; return ""; }n  onClick(handler: () => void) { this.clickHandlers.push(handler); }n}nnclass MockUIFactory implements UIFactory {n  readonly buttons:    MockButton[]   = [];n  createButton() {n    const b = new MockButton(); this.buttons.push(b); return b;n  }n  createTextInput() { return new WebTextInput(); }n  createDialog()    { return { show: () => {} }; }n}nn// ─── アプリ本体(Factory に依存し実装に依存しない)───nclass LoginForm {n  private button:    Button;n  private emailInput: TextInput;n  private dialog:    Dialog;nn  constructor(factory: UIFactory) {n    this.button     = factory.createButton();n    this.emailInput = factory.createTextInput();n    this.dialog     = factory.createDialog();n  }nn  mount() {n    this.button.render();n    this.emailInput.render();n    this.button.onClick(() => {n      const email = this.emailInput.getValue();n      this.dialog.show("確認", `${email} でログインしますか?`);n    });n  }n}nn// 本番: Web UInconst webForm  = new LoginForm(new WebUIFactory());nn// テスト: Mock UI(実際のDOMなしで動作確認)nconst mockFactory = new MockUIFactory();nconst testForm    = new LoginForm(mockFactory);ntestForm.mount();nmockFactory.buttons[0].clickHandlers[0](); // ボタンのクリックをシミュレートn

n

Builder パターン

n

Builder は「複雑なオブジェクトを段階的に構築する」パターンです。TypeScript では メソッドチェーン(fluent API)+ 型で必須フィールドを保証する高度な実装が可能です。

n

// ─── HTTPリクエスト Builder ───nntype HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";nninterface Request {n  url:     string;n  method:  HttpMethod;n  headers: Record<string, string>;n  body?:   unknown;n  timeout: number;n}nnclass RequestBuilder {n  private req: Partial<Request> = {n    headers: {},n    timeout: 5000,n  };nn  url(url: string): this {n    this.req.url = url;n    return this; // this を返すことでメソッドチェーンが可能n  }nn  method(method: HttpMethod): this {n    this.req.method = method;n    return this;n  }nn  header(key: string, value: string): this {n    this.req.headers = { ...this.req.headers, [key]: value };n    return this;n  }nn  bearer(token: string): this {n    return this.header("Authorization", `Bearer ${token}`);n  }nn  json(body: unknown): this {n    this.req.body = body;n    return this.header("Content-Type", "application/json");n  }nn  timeout(ms: number): this {n    this.req.timeout = ms;n    return this;n  }nn  build(): Request {n    if (!this.req.url)    throw new Error("url は必須です");n    if (!this.req.method) throw new Error("method は必須です");n    return this.req as Request;n  }n}nn// 利用例: メソッドチェーンで直感的に組み立てるnconst request = new RequestBuilder()n  .url("https://api.example.com/users")n  .method("POST")n  .bearer("eyJhbGciOiJSUzI1NiJ9...")n  .json({ name: "Alice", email: "alice@example.com" })n  .timeout(10_000)n  .build();n

n

型で必須フィールドを強制する高度な Builder

n

// ─── 型パラメータで「設定済みフィールド」を追跡する ───n// build() は url と method が両方設定された場合のみ呼べるnnclass TypedRequestBuilder<TSet extends string = never> {n  private req: Partial<Request> = { headers: {}, timeout: 5000 };nn  url(url: string): TypedRequestBuilder<TSet | "url"> {n    this.req.url = url;n    return this as unknown as TypedRequestBuilder<TSet | "url">;n  }nn  method(method: HttpMethod): TypedRequestBuilder<TSet | "method"> {n    this.req.method = method;n    return this as unknown as TypedRequestBuilder<TSet | "method">;n  }nn  bearer(token: string): this {n    this.req.headers = { ...this.req.headers, Authorization: `Bearer ${token}` };n    return this;n  }nn  json(body: unknown): this {n    this.req.body = body;n    this.req.headers = { ...this.req.headers, "Content-Type": "application/json" };n    return this;n  }nn  // build() は "url" と "method" が TSet に含まれている場合のみ呼べるn  build(n    this: TypedRequestBuilder<TSet & ("url" | "method")>n  ): Request {n    return this.req as Request;n  }n}nn// OK: url と method が両方セット済みnconst req = new TypedRequestBuilder()n  .url("https://api.example.com/users")n  .method("GET")n  .build(); // OKnn// Error: method が未設定のまま build() しようとするとコンパイルエラーn// new TypedRequestBuilder().url("...").build(); → Errorn

n

Builder は「段階的な構築」が必要なときに使う
コンストラクタ引数が多い(5つ以上)・引数の組み合わせが複雑・オプション引数が多い場合に Builder が有効です。引数が少なく固定的なら Builder は過設計になります。fluent API の利点は「補完が効く」「途中状態が型で管理される」「テストでの一部省略が容易」です。

n

Strategy パターン

n

Strategy は「アルゴリズムをオブジェクトとして分離し、実行時に差し替え可能にする」パターンです。TypeScript では クラスの代わりに関数型(Function Type)でシンプルかつ型安全に実装できます。

n

// ─── ソート Strategy(関数型アプローチ)───nntype SortStrategy<T> = (items: T[]) => T[];nn// 各 Strategy の実装(純粋関数)nfunction bubbleSort<T>(items: T[]): T[] {n  const arr = [...items];n  for (let i = 0; i < arr.length - 1; i++) {n    for (let j = 0; j < arr.length - 1 - i; j++) {n      if (arr[j] > arr[j + 1]) [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];n    }n  }n  return arr;n}nnfunction quickSort<T>(items: T[]): T[] {n  if (items.length <= 1) return items;n  const [pivot, ...rest] = items;n  return [n    ...quickSort(rest.filter(x => x <= pivot)),n    pivot,n    ...quickSort(rest.filter(x => x > pivot)),n  ];n}nn// Context クラス: Strategy を保持・実行nclass Sorter<T> {n  constructor(private strategy: SortStrategy<T>) {}nn  setStrategy(strategy: SortStrategy<T>): void {n    this.strategy = strategy;n  }nn  sort(items: T[]): T[] {n    return this.strategy(items);n  }n}nn// 利用例nconst sorter = new Sorter<number>(bubbleSort);nconsole.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]nnsorter.setStrategy(quickSort); // 実行時に差し替えnconsole.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]n

n

実務的な例:支払い方法の Strategy

n

// ─── 支払い Strategy(インターフェース版)───nninterface PaymentStrategy {n  readonly name:      string;n  charge(amount: number): Promise<{ success: boolean; transactionId: string }>;n  refund(transactionId: string, amount: number): Promise<boolean>;n}nnclass CreditCardStrategy implements PaymentStrategy {n  readonly name = "クレジットカード" as const;n  constructor(private cardNumber: string) {}n  async charge(amount: number) {n    // クレジットカード決済API呼び出しn    return { success: true, transactionId: `CC-${Date.now()}` };n  }n  async refund(transactionId: string) { return true; }n}nnclass PayPalStrategy implements PaymentStrategy {n  readonly name = "PayPal" as const;n  constructor(private email: string) {}n  async charge(amount: number) {n    return { success: true, transactionId: `PP-${Date.now()}` };n  }n  async refund(transactionId: string) { return true; }n}nnclass OrderService {n  private payment: PaymentStrategy;nn  constructor(payment: PaymentStrategy) {n    this.payment = payment;n  }nn  setPaymentMethod(strategy: PaymentStrategy): void {n    this.payment = strategy;n  }nn  async processOrder(totalAmount: number): Promise<string> {n    const result = await this.payment.charge(totalAmount);n    if (!result.success) throw new Error("決済失敗");n    return result.transactionId;n  }n}nn// 利用例nconst order = new OrderService(new CreditCardStrategy("4242-4242-4242-4242"));nconst txId  = await order.processOrder(3980);nn// PayPal に変更norder.setPaymentMethod(new PayPalStrategy("user@example.com"));n

n

Observer パターン(TypedEventEmitter)

n

Observer は「状態変化を複数の購読者(Observer)に通知する」パターンです。Node.js の EventEmitter は型安全性がないため、TypeScript では イベント名と payload の型をジェネリクスで紐づけた TypedEventEmitterを実装するのが実務標準です。

n

// ─── 型安全な EventEmitter ───nn// イベントマップ: イベント名 → payload 型ntype EventMap = Record<string, unknown>;nntype Listener<T> = (payload: T) => void;nnclass TypedEventEmitter<Events extends EventMap> {n  // WeakRef は使わず、Map<string, Set<Listener>> で管理n  private listeners = new Map<n    keyof Events & string,n    Set<Listener<Events[keyof Events]>>n  >();nn  on<K extends keyof Events & string>(n    event: K,n    listener: Listener<Events[K]>n  ): () => void {n    if (!this.listeners.has(event)) {n      this.listeners.set(event, new Set());n    }n    this.listeners.get(event)!.add(listener as Listener<Events[keyof Events]>);nn    // 登録解除関数を返す(useEffect の cleanup に使える)n    return () => this.off(event, listener);n  }nn  off<K extends keyof Events & string>(n    event: K,n    listener: Listener<Events[K]>n  ): void {n    this.listeners.get(event)?.delete(listener as Listener<Events[keyof Events]>);n  }nn  once<K extends keyof Events & string>(n    event: K,n    listener: Listener<Events[K]>n  ): void {n    const wrapper: Listener<Events[K]> = (payload) => {n      listener(payload);n      this.off(event, wrapper);n    };n    this.on(event, wrapper);n  }nn  emit<K extends keyof Events & string>(n    event: K,n    payload: Events[K]n  ): void {n    this.listeners.get(event)?.forEach(listener => listener(payload as Events[K]));n  }n}nn// ─── 利用例:ショッピングカートのイベント ───nn// イベントマップの定義ninterface CartEvents {n  "item:added":   { productId: string; quantity: number; price: number };n  "item:removed": { productId: string };n  "cart:cleared": undefined;n  "checkout:done": { orderId: string; total: number };n}nnconst cart = new TypedEventEmitter<CartEvents>();nn// イベント名と payload が型補完されるnconst cleanup = cart.on("item:added", ({ productId, quantity, price }) => {n  // productId: string, quantity: number, price: numbern  console.log(`追加: ${productId} × ${quantity} = ${price * quantity}円`);n});nncart.on("checkout:done", ({ orderId, total }) => {n  console.log(`注文完了: #${orderId} 合計 ${total}円`);n});nn// emit も型チェックされるncart.emit("item:added", { productId: "P001", quantity: 2, price: 1980 }); // OKn// cart.emit("item:added", { productId: "P001" }); // Error: quantity と price が欠けているn// cart.emit("unknown:event", {}); // Error: イベント名が存在しないnncleanup(); // on() の戻り値を呼ぶと登録解除n

n

Command パターン

n

Command は「操作をオブジェクトとして表現し、実行・取り消し・再実行を可能にする」パターンです。テキストエディタの Undo/Redo・トランザクション処理などに使います。

n

// ─── テキストエディタの Command パターン ───nn// Command インターフェースninterface Command {n  execute(): void;n  undo():    void;n  readonly description: string; // デバッグ用n}nn// ─── 具体的な Command 実装 ───nnclass InsertTextCommand implements Command {n  readonly description: string;nn  constructor(n    private document: { text: string },n    private position: number,n    private text:     stringn  ) {n    this.description = `Insert "${text}" at ${position}`;n  }nn  execute(): void {n    const { text, position } = this;n    this.document.text =n      this.document.text.slice(0, position) +n      text +n      this.document.text.slice(position);n  }nn  undo(): void {n    const { text, position } = this;n    this.document.text =n      this.document.text.slice(0, position) +n      this.document.text.slice(position + text.length);n  }n}nnclass DeleteTextCommand implements Command {n  readonly description: string;n  private deletedText = "";nn  constructor(n    private document: { text: string },n    private position: number,n    private length:   numbern  ) {n    this.description = `Delete ${length} chars at ${position}`;n  }nn  execute(): void {n    this.deletedText = this.document.text.slice(n      this.position, this.position + this.lengthn    );n    this.document.text =n      this.document.text.slice(0, this.position) +n      this.document.text.slice(this.position + this.length);n  }nn  undo(): void {n    this.document.text =n      this.document.text.slice(0, this.position) +n      this.deletedText +n      this.document.text.slice(this.position);n  }n}nn// ─── CommandHistory(Undo/Redo スタック)───nclass CommandHistory {n  private history: Command[] = [];n  private cursor  = -1; // 現在位置(-1 = 何もない)nn  execute(command: Command): void {n    // redo スタックを破棄(新しい操作をしたら redo できない)n    this.history = this.history.slice(0, this.cursor + 1);n    command.execute();n    this.history.push(command);n    this.cursor++;n  }nn  undo(): void {n    if (this.cursor < 0) return;n    this.history[this.cursor].undo();n    this.cursor--;n  }nn  redo(): void {n    if (this.cursor >= this.history.length - 1) return;n    this.cursor++;n    this.history[this.cursor].execute();n  }nn  canUndo(): boolean { return this.cursor >= 0; }n  canRedo(): boolean { return this.cursor < this.history.length - 1; }nn  getHistory(): readonly string[] {n    return this.history.map(c => c.description);n  }n}nn// ─── 利用例 ───nconst doc     = { text: "Hello" };nconst history = new CommandHistory();nnhistory.execute(new InsertTextCommand(doc, 5, " World"));nconsole.log(doc.text); // "Hello World"nnhistory.execute(new InsertTextCommand(doc, 11, "!"));nconsole.log(doc.text); // "Hello World!"nnhistory.undo();nconsole.log(doc.text); // "Hello World"nnhistory.undo();nconsole.log(doc.text); // "Hello"nnhistory.redo();nconsole.log(doc.text); // "Hello World"n

n

State Machine パターン

n

State Machine は「状態と遷移を明示的に定義し、無効な遷移を防ぐ」パターンです。TypeScript の Discriminated Union と型ガードを使うと、許可されていない状態遷移がコンパイル時エラーになります。

n

// ─── 注文の状態遷移マシン ───nn// 各状態をオブジェクト型で定義ntype OrderState =n  | { status: "pending";    createdAt: Date }n  | { status: "paid";       paidAt: Date;       transactionId: string }n  | { status: "shipped";    shippedAt: Date;     trackingCode: string }n  | { status: "delivered";  deliveredAt: Date }n  | { status: "cancelled";  cancelledAt: Date;   reason: string }n  | { status: "refunded";   refundedAt: Date;    amount: number };nn// 遷移イベントntype OrderEvent =n  | { type: "PAY";       transactionId: string }n  | { type: "SHIP";      trackingCode: string }n  | { type: "DELIVER" }n  | { type: "CANCEL";    reason: string }n  | { type: "REFUND";    amount: number };nn// 遷移関数: 現在の状態とイベントから次の状態を計算n// 無効な遷移は Error または null を返すnfunction transition(n  state: OrderState,n  event: OrderEventn): OrderState {n  const now = new Date();nn  switch (state.status) {n    case "pending":n      if (event.type === "PAY") {n        return { status: "paid", paidAt: now, transactionId: event.transactionId };n      }n      if (event.type === "CANCEL") {n        return { status: "cancelled", cancelledAt: now, reason: event.reason };n      }n      break;nn    case "paid":n      if (event.type === "SHIP") {n        return { status: "shipped", shippedAt: now, trackingCode: event.trackingCode };n      }n      if (event.type === "CANCEL") {n        return { status: "cancelled", cancelledAt: now, reason: event.reason };n      }n      break;nn    case "shipped":n      if (event.type === "DELIVER") {n        return { status: "delivered", deliveredAt: now };n      }n      break;nn    case "delivered":n      if (event.type === "REFUND") {n        return { status: "refunded", refundedAt: now, amount: event.amount };n      }n      break;nn    case "cancelled":n    case "refunded":n      break; // 終端状態: どのイベントも無効n  }nn  throw new Error(n    `無効な遷移: ${state.status} + ${event.type}`n  );n}nn// ─── State Machine クラス ───nclass Order {n  private state: OrderState;nn  constructor(private readonly id: string) {n    this.state = { status: "pending", createdAt: new Date() };n  }nn  dispatch(event: OrderEvent): void {n    this.state = transition(this.state, event);n  }nn  getState(): Readonly<OrderState> {n    return this.state;n  }nn  // 特定状態かどうかの型ガードn  isPaid(): this is { state: Extract<OrderState, { status: "paid" }> } {n    return this.state.status === "paid";n  }n}nn// ─── 利用例 ───nconst order = new Order("ORD-001");nn// pending → paidnorder.dispatch({ type: "PAY", transactionId: "TXN-123" });nconsole.log(order.getState().status); // "paid"nn// paid → shippednorder.dispatch({ type: "SHIP", trackingCode: "YM-987654321" });nn// 無効な遷移 → 実行時エラーn// order.dispatch({ type: "PAY", transactionId: "TXN-456" });n// → Error: 無効な遷移: shipped + PAYn

n

State Machine の状態型はDiscriminated Union ガイドの応用です。さらに複雑な状態遷移には XState(TypeScript 対応の状態機械ライブラリ)の利用も検討してください。

n

よくある質問

n

QTypeScript でデザインパターンを実装する際にクラスと関数どちらが推奨されますか?

A一概にどちらとは言えませんが、近年の TypeScript ではクラスより関数・型・インターフェースを組み合わせる傾向があります。Strategy・Observer・Command などは関数型で実装するとシンプルになります。Singleton・Builder のような「状態やライフサイクルが重要なパターン」はクラスが向いています。React でも関数コンポーネント + Hooks が標準になったように、「必要な場合だけクラス」という選択が現代的です。

n

Q「デザインパターンは不要」という意見を聞きますが本当ですか?

Aパターンを目的化すること(過設計)は確かに問題です。しかし「命名と構造の共通語」としての価値は変わりません。「Strategy パターンで実装しました」と言えばチーム全員が設計意図を理解できます。また TypeScript + デザインパターンの組み合わせでは型がパターンの制約を強制するため、意図しない実装のずれをコンパイル時に防げます。「必要なときに使う」のが正しい姿勢です。

n

QTypedEventEmitter と RxJS の Observable はどちらを使えばよいですか?

Aシンプルなイベント通知には TypedEventEmitter で十分です。RxJS の Observable は「ストリームの変換・合成・バックプレッシャー制御」が必要な場合に有効で、Angular では標準的に使われます。React + Next.js ではオーバースペックになることが多く、まず TypedEventEmitter や Zustand・Jotai などの状態管理ライブラリで対応し、複雑な非同期ストリーム処理が必要になった時点で RxJS を検討するのがよいでしょう。

n

QBuilder パターンで型パラメータを使った必須フィールド強制は難しすぎませんか?

Aチームの TypeScript レベルによります。基本の Partial<Request> + build() 内での実行時チェックで十分なプロジェクトがほとんどです。型パラメータ版(TypedRequestBuilder<TSet>)は型レベルで呼び出し順序を強制したい場合に使います。公開ライブラリや社内フレームワークの設計では有効ですが、通常のアプリケーションコードには過設計になることがあります。

n

QState Machine の遷移を TypeScript で完全に型レベルで管理できますか?

Aできますが複雑になります。transition(state: Extract<OrderState, { status: "pending" }>, event: Extract<OrderEvent, { type: "PAY" }>): Extract<OrderState, { status: "paid" }>のようにオーバーロードで各遷移を型定義すると、無効な遷移がコンパイル時エラーになります。ただし状態数が多いと組み合わせ爆発するため、実務では実行時エラーで検出する方が現実的です。型レベルの State Machine が必要なら XState v5(TypeScript 対応)の利用を推奨します。

n

まとめ

n

n

n

n

n

n

n

n

n

n

n

n

n

パターン TypeScript での実装ポイント 主な用途
Singleton private コンストラクタ + static getInstance(). ジェネリクス版で汎用化 DB接続・設定・ロガー
Factory Method Discriminated Union + switch 網羅性チェック 種別によるオブジェクト生成
Abstract Factory インターフェースで Factory の「契約」を定義。テスト時に Mock と差し替え DI・テスト容易性
Builder メソッドチェーン(fluent API)。型パラメータで必須フィールドを強制 複雑なオブジェクト構築
Strategy 関数型 or インターフェースでアルゴリズムを分離。ジェネリクスで型安全に アルゴリズム差し替え・決済方法
Observer イベントマップ型でイベント名と payload を紐づけた TypedEventEmitter リアルタイム通知・コンポーネント間通信
Command execute()/undo() インターフェース + CommandHistory で Undo/Redo エディタ・トランザクション
State Machine Discriminated Union で状態型を定義。無効な遷移は Error で検出 注文管理・ワークフロー

n

デザインパターンは目的化すると過設計になります。「このコードは後で差し替えるか?」「チームが意図を理解できるか?」を基準に適切なパターンを選択してください。TypeScript のクラスの基礎はTypeScript クラス完全ガイドを、ジェネリクスの詳細はTypeScript ジェネリクス完全ガイドを参照してください。

n