🎲

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

に公開

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

〇課題 IoMTセンサーデータの集計・変換システム
ウェアラブルデバイスから届く複数の生データ(生体情報)を整理し、異常値を除去した上で、画面表示用のサマリーを作成する関数です。 今のコードは、ループの中でフラグ管理や計算が入り乱れており、新しいデバイスタイプが増えるだけで破綻しそうな状態です。

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

// 開発環境: Vanilla TypeScript (2026-01-25)
function processSensors(data: any[]): any {
  let output = [];
  
  if (data !== null && data !== undefined && data.length > 0) {
    for (let i = 0; i < data.length; i++) {
      let item = data[i];
      let value = item.v;

      // 無効なデータのスキップ
      if (value === undefined || value === null || value < 0) {
        continue;
      }

      // デバイスごとの補正ロジック
      if (item.type === 'HEART_RATE') {
        if (value > 220) continue; // 異常値は除外
        value = Math.round(value);
      } else if (item.type === 'BLOOD_PRESSURE') {
        value = value + 2; // デバイス誤差の補正
      } else if (item.type === 'STEPS') {
        if (value < 10) continue; // 誤検知(微小な振動)を除外
      }

      // 既存のリストに同じタイプがあるか確認してマージ(なければ追加)
      let found = false;
      for (let j = 0; j < output.length; j++) {
        if (output[j].label === item.type) {
          output[j].val = (output[j].val + value) / 2; // 平均をとる
          found = true;
          break;
        }
      }

      if (!found) {
        let labelName = "";
        if (item.type === 'HEART_RATE') labelName = "心拍数";
        if (item.type === 'BLOOD_PRESSURE') labelName = "血圧";
        if (item.type === 'STEPS') labelName = "歩数";
        
        output.push({ label: item.type, name: labelName, val: value });
      }
    }
  }

  return output;
}

〇リファクタリングの指針
名著『リーダブルコード』の以下の原則を意識しました。

データの流れを パイプライン にする
forループの中で「除外」「変換」「集計」をすべて行うのではなく、filterやmapを使って段階的に処理します。

デバイスごとの設定をデータとして定義する
if-else でロジックを書くのではなく、補正値や名称を定義したマッピングオブジェクト(Record)を用意してロジックとデータを分離します。

型安全な実装
anyを排除し、デバイス種別の Union 型やインターフェースを定義して、エディタの補完を有効にします。

集計ロジックの単純化
reduceを使うことで、同一項目のマージ処理をシンプルに記述し、計算量を抑えます。

〇修正後コード

//  1. 型定義:センサーデータの構造を厳格にする 
type SensorType = "HEART_RATE" | "BLOOD_PRESSURE" | "STEPS";

interface RawReading {
  type: SensorType;
  v: number | null | undefined;
}

interface SensorSummary {
  label: SensorType;
  name: string;
  val: number;
}

//  2. 設定オブジェクト:ロジックとデータを分離する 
interface SensorConfig {
  name: string;
  correction: (v: number) => number;
  isValid: (v: number) => boolean;
}

const SENSOR_CONFIGS: Record<SensorType, SensorConfig> = {
  HEART_RATE: {
    name: "心拍数",
    correction: (v) => Math.round(v),
    isValid: (v) => v >= 0 && v <= 220,
  },
  BLOOD_PRESSURE: {
    name: "血圧",
    correction: (v) => v + 2,
    isValid: (v) => v >= 0,
  },
  STEPS: {
    name: "歩数",
    correction: (v) => v,
    isValid: (v) => v >= 10,
  },
};

//  3. メイン処理:データの流れを定義する 
function processSensors(data: RawReading[] | null | undefined): SensorSummary[] {
  if (!data || data.length === 0) return [];

  return data
    // A. 無効なデータや異常値をフィルタリング
    .filter((item): item is RawReading & { v: number } => {
      const config = SENSOR_CONFIGS[item.type];
      return item.v !== null && item.v !== undefined && config.isValid(item.v);
    })
    // B. 各デバイスごとの補正を適用
    .map((item) => ({
      type: item.type,
      value: SENSOR_CONFIGS[item.type].correction(item.v),
    }))
    // C. 同じタイプをマージして平均を算出
    .reduce((acc: SensorSummary[], curr) => {
      const config = SENSOR_CONFIGS[curr.type];
      const existing = acc.find((s) => s.label === curr.type);

      if (existing) {
        // 平均値の更新: (既存の値 + 新しい値) / 2
        existing.val = (existing.val + curr.value) / 2;
      } else {
        acc.push({
          label: curr.type,
          name: config.name,
          val: curr.value,
        });
      }
      return acc;
    }, []);
}

〇主な修正ポイント
巨大な if-else を 設定(Config)へ集約
修正前はループの中に条件分岐が散乱していましたが、SENSOR_CONFIGS に集約しました。
新しいデバイスが増えても、設定を1行追加するだけで済みます。

配列メソッドによる
パイプライン化 filter(選別)→ map(加工)→ reduce(集計)という段階に分けたことで、
「何をしているか」がメソッド名で一目瞭然になりました。

型安全(any の排除)
型定義により、プロパティ名の打ち間違いをコンパイル時に検知できます。
心拍数の制限値などのマジックナンバーも型と設定の中に修正しました。

集計ロジックの効率化
修正前は二重ループに近い動きをしていましたが、reduce 内での集計にまとめることで、データの流れがより明確になりました。

〇参照先
▼公式ドキュメント

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array

https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures

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

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

以上

Discussion