JavaScript includes()メソッドの実践ガイド|業務で使える配列検索テクニック

JavaScript

JavaScript includes()メソッドの実践ガイド|業務で使える配列検索テクニック

JavaScriptのincludes()メソッドは、配列に特定の要素が含まれているかを判定するシンプルながら非常に便利なメソッドです。実務開発では頻繁に登場し、正しく使いこなすことで可読性の高いコードを実現できます。本記事では、基本から実践的なユースケース、応用パターンまでを網羅的に解説します。

1. includes()メソッドの基礎知識

includes()メソッドは、配列内に指定の要素が存在するかどうかをBoolean値(true/false)で返します。ES2016で標準化された比較的新しいメソッドですが、モダンブラウザであれば問題なく使用できます。

// 基本的な使い方
const fruits = ['apple', 'banana', 'orange'];
console.log(fruits.includes('apple'));  // true
console.log(fruits.includes('grape'));   // false

// 第2引数で検索開始位置を指定
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.includes(3, 2));     // true(インデックス2から検索)
console.log(numbers.includes(2, 3));     // false(インデックス3以降に2がない)

includes()は内部的に厳密等価演算子(===)を使用して比較を行います。そのため、オブジェクトや配列などの参照型では注意が必要です。

2. 業務でのユースケース

2-1. ユーザー権限管理

Webアプリケーションで最も一般的なユースケースが権限管理です。ユーザーが特定の機能にアクセスできるかを判定する際にincludes()が活躍します。

// ユーザーの権限リスト
const userPermissions = ['read', 'write', 'comment'];

// 権限チェック関数
function hasPermission(permission) {
  return userPermissions.includes(permission);
}

// 使用例
if (hasPermission('delete')) {
  console.log('記事を削除できます');
} else {
  console.log('削除権限がありません');
}

// より実践的:複数の権限をチェック
function hasAnyPermission(requiredPermissions) {
  return requiredPermissions.some(permission => 
    userPermissions.includes(permission)
  );
}

function hasAllPermissions(requiredPermissions) {
  return requiredPermissions.every(permission => 
    userPermissions.includes(permission)
  );
}

// 使用例
if (hasAllPermissions(['read', 'write'])) {
  console.log('記事を編集できます');
}

2-2. フォーム入力値の検証

ユーザーからの入力値が許可された値に含まれているかを検証する際に便利です。

// 許可された国のリスト
const allowedCountries = ['JP', 'US', 'GB', 'FR', 'DE', 'CN'];

// フォーム送信時の検証
function validateCountryCode(countryCode) {
  if (!allowedCountries.includes(countryCode.toUpperCase())) {
    throw new Error('対応していない国コードです');
  }
  return true;
}

// 実装例
try {
  validateCountryCode('jp');  // 成功(大文字に変換)
  validateCountryCode('kr');  // エラー
} catch (error) {
  console.error(error.message);
}

2-3. 配列フィルタリング

特定の条件に基づいて配列の要素を取得する際に使用します。

// 商品カテゴリーリスト
const products = [
  { id: 1, name: '書籍', category: 'books' },
  { id: 2, name: 'ペン', category: 'stationery' },
  { id: 3, name: 'ノート', category: 'stationery' },
  { id: 4, name: '雑誌', category: 'books' },
];

// 特定のカテゴリー内の商品を抽出
const targetCategories = ['books', 'electronics'];
const filteredProducts = products.filter(product =>
  targetCategories.includes(product.category)
);

console.log(filteredProducts);
// [{ id: 1, name: '書籍', category: 'books' }, { id: 4, name: '雑誌', category: 'books' }]

3. TypeScriptを使った実装コード

TypeScriptを使うことで、型安全性を確保しながらより堅牢なコードを書くことができます。

// ユーザーロールの定義
type UserRole = 'admin' | 'moderator' | 'user' | 'guest';

class PermissionManager {
  private userRoles: UserRole[];

  constructor(roles: UserRole[]) {
    this.userRoles = roles;
  }

  // 単一ロールの確認
  hasRole(role: UserRole): boolean {
    return this.userRoles.includes(role);
  }

  // 複数ロールのいずれかを持っているか
  hasAnyRole(...roles: UserRole[]): boolean {
    return roles.some(role => this.userRoles.includes(role));
  }

  // 複数ロールをすべて持っているか
  hasAllRoles(...roles: UserRole[]): boolean {
    return roles.every(role => this.userRoles.includes(role));
  }

  // 機能へのアクセス可否判定
  canAccess(feature: string): boolean {
    const featureAccessMap: Record = {
      admin: ['dashboard', 'settings', 'users', 'analytics'],
      moderator: ['dashboard', 'comments', 'reports'],
      user: ['dashboard', 'profile'],
      guest: ['home'],
    };

    for (const role of this.userRoles) {
      if (featureAccessMap[role].includes(feature)) {
        return true;
      }
    }
    return false;
  }
}

// 使用例
const userPermissions = new PermissionManager(['user', 'moderator']);
console.log(userPermissions.hasRole('admin'));        // false
console.log(userPermissions.hasAnyRole('admin', 'user'));  // true
console.log(userPermissions.canAccess('dashboard'));  // true
console.log(userPermissions.canAccess('settings'));   // false

4. Pythonでの類似実装

JavaScriptのincludes()は、Pythonのin演算子に相当します。実装の違いを理解することで、言語間の相互理解が深まります。

from typing import List, Literal

UserRole = Literal['admin', 'moderator', 'user', 'guest']

class PermissionManager:
    def __init__(self, roles: List[UserRole]):
        self.user_roles = roles

    def has_role(self, role: UserRole) -> bool:
        # Pythonの 'in' はJavaScriptの includes() に相当
        return role in self.user_roles

    def has_any_role(self, *roles: UserRole) -> bool:
        return any(role in self.user_roles for role in roles)

    def has_all_roles(self, *roles: UserRole) -> bool:
        return all(role in self.user_roles for role in roles)

    def can_access(self, feature: str) -> bool:
        feature_access_map = {
            'admin': ['dashboard', 'settings', 'users', 'analytics'],
            'moderator': ['dashboard', 'comments', 'reports'],
            'user': ['dashboard', 'profile'],
            'guest': ['home'],
        }

        for role in self.user_roles:
            if feature in feature_access_map.get(role, []):
                return True
        return False

# 使用例
user_perms = PermissionManager(['user', 'moderator'])
print(user_perms.has_role('admin'))        # False
print(user_perms.has_any_role('admin', 'user'))  # True
print(user_perms.can_access('dashboard'))  # True

5. よくある応用パターン

5-1. ホワイトリスト・ブラックリスト処理

入力値を制限する際にホワイトリストまたはブラックリストを使用します。

// メールドメインのホワイトリスト(企業メールのみ許可)
class EmailValidator {
  private allowedDomains = ['company.com', 'partner.co.jp'];
  private blockedDomains = ['tempmail.com', 'throwaway.email'];

  validateEmail(email) {
    const domain = email.split('@')[1];

    // ブラックリストチェック(優先度高)
    if (this.blockedDomains.includes(domain)) {
      return { valid: false, reason: '使用不可のメールドメインです' };
    }

    // ホワイトリストチェック
    if (!this.allowedDomains.includes(domain)) {
      return { valid: false, reason: '許可されたドメインのみ使用可能です' };
    }

    return { valid: true, reason: '検証成功' };
  }
}

// 使用例
const validator = new EmailValidator();
console.log(validator.validateEmail('user@company.com'));     // valid: true
console.log(validator.validateEmail('user@tempmail.com'));    // valid: false

5-2. デバイス・ブラウザの互換性チェック

対応デバイスやブラウザを確認する際に活用できます。

class DeviceChecker {
  private supportedDevices = ['iOS', 'Android', 'Windows', 'macOS'];
  private supportedBrowsers = ['Chrome', 'Firefox', 'Safari', 'Edge'];

  isDeviceSupported(deviceType) {
    return this.supportedDevices.includes(deviceType);
  }

  isBrowserSupported(browserName) {
    return this.supportedBrowsers.includes(browserName);
  }

  canRunApp(deviceType, browserName) {
    return this.isDeviceSupported(deviceType) && 
           this.isBrowserSupported(browserName);
  }
}

// 実装例
const checker = new DeviceChecker();
console.log(checker.canRunApp('iOS', 'Safari'));      // true
console.log(checker.canRunApp('Linux', 'Chrome'));    // false

5-3. ステータス遷移の検証

ステートマシンの実装で有効な状態遷移かを確認します。

class OrderStatusManager {
  private currentStatus = 'pending';

  // 各ステータスから遷移可能な状態
  private validTransitions = {
    'pending': ['confirmed', 'cancelled'],
    'confirmed': ['shipped', 'cancelled'],
    'shipped': ['delivered'],
    'delivered': [],
    'cancelled': [],
  };

  canTransition(toStatus) {
    return this.validTransitions[this.currentStatus].includes(toStatus);
  }

  transition(toStatus) {
    if (!this.canTransition(toStatus)) {
      throw new Error(`${this.currentStatus} から ${toStatus} への遷移は不可能です`);
    }
    this.currentStatus = toStatus;
    console.log(`ステータスが ${toStatus} に更新されました`);
  }

  getStatus() {
    return this.currentStatus;
  }
}

// 使用例
const order = new OrderStatusManager();
order.transition('confirmed');    // 成功
order.transition('shipped');      // 成功
order.transition('pending');      // エラー

6. 注意点と落とし穴

6-1. オブジェクト・配列の比較

includes()は参照値を比較するため、オブジェクトや配列の要素には機能しません。

// ❌ うまく動作しない例
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
];

console.log(users.includes({ id: 1, name: 'Alice' }));  // false(オブジェクト参照が異なる)

// ✅ 正しい実装方法1:idで検索
const userId = 1;
const userExists = users.some(user => user.id === userId);
console.log(userExists);  // true

// ✅ 正しい実装方法2:JSON文字列化(ただし効率は落ちる)
const targetUser = { id: 1, name: 'Alice' };
const existsByJson = users.some(user =>
  JSON.stringify(user) === JSON.stringify(targetUser)
);
console.log(existsByJson);  // true

6-2. NaN値の扱い

includes()はNaNとの比較に特別な処理が行われます。

// NaNは通常の比較では等値にならない
console.log(NaN === NaN);  // false

// しかし includes() はNaNを検出できる
const values = [1, 2, NaN, 4];
console.log(values.includes(NaN));  // true

// indexOfではNaNを検出できない(こちらは === を使用)
console.log(values.indexOf(NaN));   // -1

6-3. パフォーマンスへの配慮

大量の要素を検索する場合、Setを使用すると効率的です。

// 大規模なホワイトリスト
const largeWhitelist = ['item1', 'item2', ..., 'item100000'];

// ❌ 大量検索の場合は遅い(O(n)の計算量)
function isInWhitelist_slow(item) {
  return largeWhitelist.includes(item);
}

// ✅ Setを使用すると高速(O(1)の計算量)
const whitelistSet = new Set(largeWhitelist);
function isInWhitelist_fast(item) {
  return whitelistSet.has(item);
}

// パフォーマンス比較
console.time('配列');
for (let i = 0; i < 10000; i++) {
  isInWhitelist_slow('item99999');
}
console.timeEnd('配列');

console.time('Set');
for (let i = 0; i < 10000; i++) {
  isInWhitelist_fast('item99999');
}
console.timeEnd('Set');

6-4. ケースセンシティビティの問題

文字列の大文字小文字を考慮する必要があります。

// ❌ ケースセンシティブな判定
const statuses = ['active', 'inactive', 'pending'];
console.log(statuses.includes('Active'));  // false

// ✅ ケースインセンシティブな判定
function includesIgnoreCase(array, searchValue) {
  return array.some(item =>
    typeof item === 'string' && item.toLowerCase() === searchValue.toLowerCase()
  );
}

console.log(includesIgnoreCase(statuses, 'Active'));  // true

7. 実務での総合例

ここまでの知識を組み合わせた、実務で実際に使用されるコード例を紹介します。

// Eコマースサイトの注文管理システム
class OrderValidationService {
  private allowedCountries = ['JP', 'US', 'GB'];
  private allowedPaymentMethods = ['credit_card', 'bank_transfer', 'digital_wallet'];
  private restrictedItems = ['restricted_item_1', 'restricted_item_2'];
  private vipDiscountEligibleStatus = ['platinum', 'gold'];

  validateOrder(orderData) {
    const errors = [];

    // 国の検証
    if (!this.allowedCountries.includes(orderData.country.toUpperCase())) {
      errors.push('お届け先の国が対応していません');
    }

    // 決済方法の検証
    if (!this.allowedPaymentMethods.includes(orderData.paymentMethod)) {
      errors.push('選択できない決済方法です');
    }

    // 制限商品のチェック
    const hasRestrictedItems = orderData.items.some(item =>
      this.restrictedItems.includes(item.sku)
    );
    if (hasRestrictedItems) {
      errors.push('購入できない商品が含まれています');
    }

    // VIP割引適用判定
    const isVipEligible = this.vipDiscountEligibleStatus.includes(
      orderData.customerStatus
    );

    return {
      isValid: errors.length === 0,
      errors,
      vipEligible: isVipEligible,
    };
  }
}

// 使用例
const validationService = new OrderValidationService();
const result = validationService.validateOrder({
  country: 'jp',
  paymentMethod: 'credit_card',
  items: [
    { sku: 'SKU001' },
    { sku: 'SKU002' },
  ],
  customerStatus: 'gold',
});

console.log(result);
// {
//   isValid: true,
//   errors: [],
//   vipEligible: true
// }

8. まとめ

includes()メソッドはシンプルながら、実務開発の様々な場面で活躍する重要なメソッドです。本記事で紹介したポイントをまとめます。

  • 基本的な用途:配列に要素が含まれているかの判定
  • 実務での活用:権限管理、バリデーション、フィルタリング、ステータス管理など
  • パフォーマンス:大規模データではSetの使用を検討
  • 注意点:オブジェクト比較、NaN処理、ケースセンシティビティに配慮が必要
  • 型安全性:TypeScriptでより堅牢なコードを実装可能

これらの知識を活用することで、より読みやすく保守性の高い実務コードを書くことができます。特にバリデーション周りの実装では、includes()を中心にしたシンプルで明確なコード構造を心がけることが大切です。プロジェクトの規模や要件に応じて、適切な使い分けを行うことをお勧めします。

タイトルとURLをコピーしました