🙌

【15日目】『リーダブルコード』を意識してTypescriptをリファクタリングしてみた

に公開

技術ブログ15日目。
本日は、Typescriptの読みやすさを追求します。
開発環境:Vanilla TypeScript

〇課題
マルチデバイス通知送付システム
ユーザーの設定(メール、SMS、プッシュ通知)に合わせて、メッセージの優先度に応じた送付方法を決定する関数です。 今のコードは、送付手段が増えるたびに if 文が巨大化し、どこで何を判定しているのか非常に分かりにくい状態です。

〇修正前コード(動くけれど読みにくいコード)

// 開発環境: Vanilla TypeScript
function notify(u: any, m: any, t: any): any {
  let res = { s: false, log: "" };

  if (u !== null && u !== undefined) {
    if (u.active === true) {
      // 優先度が緊急(1)の場合
      if (t === 1) {
        console.log("緊急通知を発信します");
        if (u.email !== "") {
          res.log += "Email送信完了: " + m;
          res.s = true;
        }
        if (u.phone !== "") {
          res.log += " / SMS送信完了: " + m;
          res.s = true;
        }
      } else {
        // 通常通知(2)の場合
        if (u.pref === "EMAIL") {
          res.log = "Emailで送信しました: " + m;
          res.s = true;
        } else if (u.pref === "PUSH") {
          res.log = "プッシュ通知で送信しました: " + m;
          res.s = true;
        } else {
          res.log = "通知設定が未設定です";
        }
      }
    } else {
      res.log = "ユーザーが非アクティブです";
    }
  } else {
    res.log = "ユーザー情報エラー";
  }

  return res;
}

〇リファクタリングの指針
型定義の徹底
通知の優先度や送付手段に名前を付け、Union型で定義することで、マジックナンバー(意味不明な数字)を排除します。

早期リターンの適用
例外ケースを関数の冒頭で排除し、メインロジックのネスト(階層)を浅くします。

関心の分離(関数の抽出)
「緊急時のルール」と「通常時のルール」を別の関数に切り出し、役割を明確にします。

文字列構築の改善
+= による複雑な結合をやめ、テンプレートリテラルと配列の結合を活用します。

〇修正後コード

//  1. 型定義:意味のある名前をつける 
type NotificationPriority = "EMERGENCY" | "NORMAL";
type NotificationMethod = "EMAIL" | "PUSH";

interface User {
  id: string;
  name: string;
  isActive: boolean;
  email: string;
  phoneNumber: string;
  preference: NotificationMethod;
}

interface NotificationResult {
  isSuccess: boolean;
  log: string;
}

// 2. メイン処理:条件分岐をスッキリさせる
function sendNotification(
  user: User | null | undefined,
  message: string,
  priority: NotificationPriority
): NotificationResult {
  // ガード節1:ユーザーの存在確認
  if (!user) {
    return { isSuccess: false, log: "ユーザー情報エラー" };
  }

  // ガード節2:アクティブ状態の確認
  if (!user.isActive) {
    return { isSuccess: false, log: "ユーザーが非アクティブです" };
  }

  // 優先度によって処理を完全に分ける(塊で捉える)
  if (priority === "EMERGENCY") {
    return sendEmergencyNotification(user, message);
  }

  return sendNormalNotification(user, message);
}

// 3. サブ処理:専門の送付ロジック(関数の抽出) 

/** 緊急時の通知(利用可能なすべての手段で送る) */
function sendEmergencyNotification(user: User, message: string): NotificationResult {
  const sentMethods: string[] = [];

  if (user.email) {
    sentMethods.push(`Email送信完了: ${message}`);
  }
  if (user.phoneNumber) {
    sentMethods.push(`SMS送信完了: ${message}`);
  }

  if (sentMethods.length === 0) {
    return { isSuccess: false, log: "緊急通知の送付手段がありません" };
  }

  return { isSuccess: true, log: sentMethods.join(" / ") };
}

/** 通常時の通知(ユーザーの好みに合わせて送る) */
function sendNormalNotification(user: User, message: string): NotificationResult {
  if (user.preference === "EMAIL") {
    return { isSuccess: true, log: `Emailで送信しました: ${message}` };
  }
  if (user.preference === "PUSH") {
    return { isSuccess: true, log: `プッシュ通知で送信しました: ${message}` };
  }

  return { isSuccess: false, log: "通知設定が未設定です" };
}

〇主な修正ポイント

  1. マジックナンバーをなくし、Union型へ
    修正前は t === 1 という数字を使っていましたが、"EMERGENCY" | "NORMAL" という型を定義しました。これにより、コードを読むだけで「緊急時の処理」であることが一瞬でわかるようになります。

  2. ネストを解消するガード節
    if文の中にif文が重なるピラミッド構造を解消しました。不適切な状態を関数の最初で追い返すことで、メインの送付ロジックが非常に読みやすくなっています。

  3. 関心の分離
    緊急時と通常時のロジックを別々の関数に切り出しました。これにより、例えば「通常通知に新しいSNS連携を追加したい」といった場合でも、緊急通知側のコードを壊す心配がなくなります。

  4. 文字列構築のクリーン化
    配列にメッセージを溜めて最後にjoinする方法を採用しました。区切り文字( / )の扱いや、条件によるメッセージの増減が非常にシンプルに管理できるようになっています。

〇参照先
▼公式ドキュメント
https://www.typescriptlang.org/docs/handbook/enums.html

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

▼書籍
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック

以上

Discussion