Skip to content

実用的な変換パターン

変換オペレーターは、RxJSにおいて最も頻繁に使用されるオペレーター群の一つです。
リアクティブプログラミングにおいて、データを柔軟に加工・変形するために不可欠な役割を果たします。

このセクションでは、典型的な実践例を紹介しながら、変換オペレーターの活用パターンを整理します。

💬 典型的な活用パターン

パターン代表的なオペレーター説明
値の単純変換map各値に変換関数を適用
累積・集計処理scan, reduce値を逐次的に蓄積
ネスト非同期処理mergeMap, switchMap, concatMap, exhaustMapObservableを生成・結合
バッチ処理・グループ化bufferTime, bufferCount, windowTimeまとめて処理・分割管理
プロパティ抽出pluckオブジェクトから特定フィールド抽出

ユーザー入力のバリデーションと変換

ts
import { fromEvent } from 'rxjs';
import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';

// 入力フィールド
const emailInput = document.createElement('input');
const emailStatus = document.createElement('p');
document.body.appendChild(emailInput);
document.body.appendChild(emailStatus);

// メールアドレスのバリデーション関数
function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// 入力処理
fromEvent(emailInput, 'input')
  .pipe(
    debounceTime(400),
    map((event) => (event.target as HTMLInputElement).value.trim()),
    distinctUntilChanged(),
    map((email) => {
      if (!email) {
        return {
          isValid: false,
          message: 'メールアドレスを入力してください',
          value: email,
        };
      }

      if (!isValidEmail(email)) {
        return {
          isValid: false,
          message: '有効なメールアドレスを入力してください',
          value: email,
        };
      }

      return {
        isValid: true,
        message: 'メールアドレスは有効です',
        value: email,
      };
    })
  )
  .subscribe((result) => {
    if (result.isValid) {
      emailStatus.textContent = '✓ ' + result.message;
      emailStatus.className = 'valid';
    } else {
      emailStatus.textContent = '✗ ' + result.message;
      emailStatus.className = 'invalid';
    }
  });

オブジェクト配列の変換と集計

ts
import { from } from 'rxjs';
import { map, toArray } from 'rxjs/operators';

// 売上データ
const sales = [
  { product: 'ノートPC', price: 120000, quantity: 3 },
  { product: 'タブレット', price: 45000, quantity: 7 },
  { product: 'スマートフォン', price: 85000, quantity: 4 },
  { product: 'マウス', price: 3500, quantity: 12 },
  { product: 'キーボード', price: 6500, quantity: 8 },
];

// データ変換と集計
from(sales)
  .pipe(
    // 各商品の合計金額を計算
    map((item) => ({
      product: item.product,
      price: item.price,
      quantity: item.quantity,
      total: item.price * item.quantity,
    })),
    // 税込価格を追加
    map((item) => ({
      ...item,
      totalWithTax: Math.round(item.total * 1.1),
    })),
    // 配列に戻す
    toArray(),
    // 合計金額を計算
    map((items) => {
      const grandTotal = items.reduce((sum, item) => sum + item.total, 0);
      const grandTotalWithTax = items.reduce(
        (sum, item) => sum + item.totalWithTax,
        0
      );
      return {
        items,
        grandTotal,
        grandTotalWithTax,
      };
    })
  )
  .subscribe((result) => {
    console.log('商品詳細:', result.items);
    console.log('合計金額(税抜):', result.grandTotal);
    console.log('合計金額(税込):', result.grandTotalWithTax);
  });
// 出力:
// 商品詳細: (5) [{…}, {…}, {…}, {…}, {…}]
// 合計金額(税抜): 1109000
// 合計金額(税込): 1219900

JSONデータの正規化

ts
import { ajax } from 'rxjs/ajax';
import { map } from 'rxjs/operators';

const resultBox = document.createElement('div');
resultBox.id = 'normalized-results';
document.body.appendChild(resultBox);

ajax
  .getJSON<any[]>('https://jsonplaceholder.typicode.com/users')
  .pipe(
    map((users) => {
      // IDをキーとするオブジェクトに変換
      const normalizedUsers: Record<number, any> = {};
      const userIds: number[] = [];

      users.forEach((user) => {
        normalizedUsers[user.id] = {
          ...user,
          // ネストしたオブジェクトを平坦化
          companyName: user.company.name,
          city: user.address.city,
          street: user.address.street,
          // 不要なネストを削除
          company: undefined,
          address: undefined,
        };
        userIds.push(user.id);
      });

      return {
        entities: normalizedUsers,
        ids: userIds,
      };
    })
  )
  .subscribe((result) => {
    const title = document.createElement('h3');
    title.textContent = '正規化されたユーザーデータ';
    resultBox.appendChild(title);

    result.ids.forEach((id) => {
      const user = result.entities[id];
      const div = document.createElement('div');
      div.innerHTML = `
      <strong>${user.name}</strong><br>
      ユーザー名: @${user.username}<br>
      Email: ${user.email}<br>
      会社: ${user.companyName}<br>
      住所: ${user.city}, ${user.street}<br><br>
    `;
      resultBox.appendChild(div);
    });

    // 特定のIDのユーザーに素早くアクセス可能
    console.log('ユーザーID 3:', result.entities[3]);
  });

複数の変換の組み合わせ

実際のアプリケーションでは、複数の変換オペレーターを組み合わせて使用することが一般的です。

ts
import { fromEvent, timer } from 'rxjs';
import {
  switchMap,
  map,
  tap,
  debounceTime,
  takeUntil,
  distinctUntilChanged,
} from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';

type User = {
  id: number;
  name: string;
  username: string;
  email: string;
  company: {
    name: string;
  };
};

// 検索入力
const searchInput = document.createElement('input');
const resultsContainer = document.createElement('p');
const loadingIndicator = document.createElement('p');

document.body.append(searchInput);
document.body.append(resultsContainer);
document.body.append(loadingIndicator);

// 検索処理
fromEvent(searchInput, 'input')
  .pipe(
    // 入力値を取得
    map((event) => (event.target as HTMLInputElement).value.trim()),
    // 300ms待機
    debounceTime(300),
    // 同じ値なら無視
    distinctUntilChanged(),
    // ローディング表示
    tap(() => {
      loadingIndicator.style.display = 'block';
      resultsContainer.innerHTML = '';
    }),
    // APIリクエスト(前のリクエストはキャンセル)
    switchMap((term) => {
      // 空の入力は結果なし
      if (term === '') {
        return [];
      }

      // タイムアウト処理(5秒)
      const timeout$ = timer(5000).pipe(
        tap(() => console.warn('APIレスポンスがタイムアウトしました')),
        map(() => [{ error: 'タイムアウト' }])
      );

      // API呼び出し
      const response$ = ajax
        .getJSON(
          `https://jsonplaceholder.typicode.com/users?username_like=${term}`
        )
        .pipe(
          // 結果を加工
          map((users) =>
            (users as User[]).map((user) => ({
              id: user.id,
              name: user.name,
              username: user.username,
              email: user.email,
              company: user.company.name,
            }))
          ),
          // タイムアウトまでに完了
          takeUntil(timeout$)
        );

      return response$;
    }),
    // ローディング終了
    tap(() => {
      loadingIndicator.style.display = 'none';
    })
  )
  .subscribe((result) => {
    loadingIndicator.style.display = 'none';

    if (Array.isArray(result)) {
      if (result.length === 0) {
        resultsContainer.innerHTML =
          '<div class="no-results">ユーザーが見つかりませんでした</div>';
      } else {
        resultsContainer.innerHTML = result
          .map(
            (user) => `
          <div class="user-card">
            <h3>${user.name}</h3>
            <p>@${user.username}</p>
            <p>${user.email}</p>
            <p>会社: ${user.company}</p>
          </div>
        `
          )
          .join('');
      }
    } else {
      resultsContainer.innerHTML = `<div class="error">⚠️ ${result}</div>`;
    }
  });

🧠 まとめ

  • 単純な変換は map
  • 非同期を扱うなら mergeMapswitchMapconcatMapexhaustMap
  • バッチ処理は bufferTimebufferCount
  • プロパティ抽出は pluck
  • 実際のアプリではこれらを組み合わせることが常態

変換オペレーターをマスターすると、複雑な非同期データフローも
直感的かつ宣言的に扱えるようになります!

Released under the CC-BY-4.0 license.