【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 内での集計にまとめることで、データの流れがより明確になりました。
〇参照先
▼公式ドキュメント
▼書籍
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック
以上
Discussion