JavaScript Tips | 配列ユーティリティ:インデックス付与

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「インデックス付与」

「インデックス付与」は、配列の各要素に「何番目か」という情報をくっつける処理です。
JavaScript の配列はもともと 0, 1, 2…というインデックスを持っていますが、それは「配列の外側の情報」です。

業務では、次のような場面で「インデックスを“データとして”持たせたい」ことがよくあります。

  • 画面表示用の「連番(1, 2, 3…)」を付けたい
  • CSV 出力で「行番号」を一緒に出したい
  • ソートやフィルタ後も「元の順番」を覚えておきたい

こういうときに、「インデックス付与ユーティリティ」があると、とてもスッキリ書けます。


基本の考え方:map で「index をくっつける」

map の第2引数をちゃんと使う

Array.prototype.map は、コールバックに「要素」と「インデックス」を渡してくれます。

array.map((item, index) => {
  // item: 要素
  // index: 0, 1, 2, ...
});
JavaScript

これを使えば、「元の要素に index をくっつけた新しいオブジェクト」を簡単に作れます。

一番シンプルなインデックス付与ユーティリティ

function withIndex(array, indexKey = "index", start = 0) {
  if (!Array.isArray(array)) {
    return [];
  }

  return array.map((item, i) => {
    const idx = start + i;
    if (item != null && typeof item === "object") {
      return { ...item, [indexKey]: idx };
    }
    return { value: item, [indexKey]: idx };
  });
}
JavaScript

ここでやっていることをかみ砕きます。

  • indexKey: インデックスを格納するプロパティ名(デフォルト "index"
  • start: 何から数え始めるか(デフォルト 0。画面用なら 1 にすることも多い)
  • オブジェクトならスプレッドでプロパティ追加、プリミティブなら { value, index } という形に包む

これで、「どんな配列でも、とりあえずインデックス付きの配列に変換できる」ようになります。

実際の動き

const users = [
  { name: "Alice" },
  { name: "Bob" },
  { name: "Charlie" },
];

withIndex(users);
/*
[
  { name: "Alice", index: 0 },
  { name: "Bob", index: 1 },
  { name: "Charlie", index: 2 },
]
*/

withIndex(users, "rowNo", 1);
/*
[
  { name: "Alice", rowNo: 1 },
  { name: "Bob", rowNo: 2 },
  { name: "Charlie", rowNo: 3 },
]
*/
JavaScript

画面表示用の「1 から始まる連番」を付けたいときは、start = 1 にするのが定番です。


「元の順番を覚えておく」ためのインデックス付与

ソートやフィルタ後に「元の順番」に戻したい

業務でよくあるのが、「一度ソートやフィルタをしたあとで、元の順番に戻したい」というケースです。

例えば:

  • 一覧を「名前順」にソートしたけど、「元の登録順」に戻したくなる
  • 一度フィルタしたけど、「元の並び順で CSV 出力したい」

このとき、「元のインデックス」をデータとして持っておくと、とても簡単に戻せます。

originalIndex を付けるユーティリティ

function withOriginalIndex(array, indexKey = "originalIndex") {
  if (!Array.isArray(array)) {
    return [];
  }

  return array.map((item, i) => {
    if (item != null && typeof item === "object") {
      return { ...item, [indexKey]: i };
    }
    return { value: item, [indexKey]: i };
  });
}
JavaScript

これで、「今の配列の順番」を originalIndex として保存できます。

実際の使い方

const users = [
  { name: "Bob" },     // 0
  { name: "Alice" },   // 1
  { name: "Charlie" }, // 2
];

const withIdx = withOriginalIndex(users);
/*
[
  { name: "Bob", originalIndex: 0 },
  { name: "Alice", originalIndex: 1 },
  { name: "Charlie", originalIndex: 2 },
]
*/

// 名前順にソート
const sorted = [...withIdx].sort((a, b) =>
  a.name.localeCompare(b.name)
);
/*
[
  { name: "Alice", originalIndex: 1 },
  { name: "Bob", originalIndex: 0 },
  { name: "Charlie", originalIndex: 2 },
]
*/

// 元の順番に戻したいとき
const restored = [...sorted].sort(
  (a, b) => a.originalIndex - b.originalIndex
);
JavaScript

ポイントは、「ソート前に originalIndex を付けておく」ことです。
これをやっておけば、どんなにソートやフィルタをしても、「元の順番」に戻すことができます。


表示用の「連番」としてのインデックス付与

画面に「No.1, No.2, …」を出したい

一覧画面で、「No.1」「No.2」のような連番を表示したいことはよくあります。
このとき、テンプレート側で「インデックスを計算する」のもアリですが、
ユーティリティであらかじめ付けておくと、ビューがシンプルになります。

function withDisplayIndex(array, key = "no", start = 1) {
  return withIndex(array, key, start);
}
JavaScript

withIndex の薄いラッパーですが、「これは画面表示用の番号だよ」という意図を名前で伝えられます。

ページングと組み合わせるときの注意

ページングされた一覧で「全体の何番目か」を表示したいときは、
「ページ番号」と「1ページあたり件数」から開始番号を計算します。

function withGlobalIndex(array, page, pageSize, key = "no") {
  const start = (page - 1) * pageSize + 1;
  return withIndex(array, key, start);
}
JavaScript

例えば、「2ページ目」「1ページあたり 10 件」のときは、start = 11 になります。

const pageItems = [
  { name: "User11" },
  { name: "User12" },
];

withGlobalIndex(pageItems, 2, 10);
/*
[
  { name: "User11", no: 11 },
  { name: "User12", no: 12 },
]
*/
JavaScript

ここが実務での重要ポイントで、「ページ内のインデックス」ではなく「全体でのインデックス」をどう計算するか」です。
(page - 1) * pageSize + 1 という式を覚えておくと、どの画面でも同じロジックで使い回せます。


インデックス付与で気をつけたいこと

元のオブジェクトを「直接書き換える」かどうか

今までの実装は、すべて「新しいオブジェクトを作る」スタイルでした。

{ ...item, index: idx }
JavaScript

これは「元のデータを壊さない」ので、安全です。
一方で、「パフォーマンス重視で、元のオブジェクトに直接プロパティを足したい」場面もあります。

function attachIndexInPlace(array, indexKey = "index", start = 0) {
  if (!Array.isArray(array)) return array;

  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    const idx = start + i;
    if (item != null && typeof item === "object") {
      item[indexKey] = idx;
    }
  }

  return array;
}
JavaScript

これは「破壊的」なので、使うときは「ここで index を付けると、以降ずっと残る」ことを意識する必要があります。
基本は「非破壊(新しい配列を返す)」をデフォルトにしておき、
どうしても必要なときだけ InPlace 系の関数を使う、という設計がおすすめです。

インデックスの意味を名前で伝える

インデックスには、いろいろな意味があります。

  • 配列内の位置(0 始まり)
  • 表示用の連番(1 始まり)
  • 元の順番(originalIndex)
  • ページングをまたいだ通し番号(no, rowNo など)

これらを全部 index という名前で持たせてしまうと、あとで読んだ人が混乱します。

  • index … 単純な 0 始まりの位置
  • no / rowNo … 表示用の 1 始まり連番
  • originalIndex … ソート前の位置
  • globalNo … 全体での通し番号

のように、「何を表すインデックスなのか」をプロパティ名で表現してあげると、
未来の自分がかなり助かります。


少し手を動かして感覚をつかむ

コンソールで、次のようなコードを実際に打ってみてください。

const users = [
  { name: "Bob" },
  { name: "Alice" },
  { name: "Charlie" },
];

withIndex(users);
withIndex(users, "rowNo", 1);
withOriginalIndex(users);

const withIdx = withOriginalIndex(users);
const sorted = [...withIdx].sort((a, b) => a.name.localeCompare(b.name));
const restored = [...sorted].sort((a, b) => a.originalIndex - b.originalIndex);

const pageItems = [
  { name: "User11" },
  { name: "User12" },
];

withGlobalIndex(pageItems, 2, 10);
JavaScript

「どんなインデックスが付いているか」「ソートしても元の順番に戻せるか」「ページングと組み合わせたときに番号がどう付くか」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function withIndex(...) { ... }
export function withOriginalIndex(...) { ... }
export function withGlobalIndex(...) { ... }
JavaScript

のような関数を置き、

「インデックスをデータとして持たせたくなったら、必ずこの“インデックス付与ユーティリティ”を通す」

というルールを作ってみてください。
そうすると、「なんとなく map の第2引数をその場で使う」状態から、「意図のはっきりしたインデックス設計」に一段ステップアップしていきます。

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