何をしたいユーティリティか:「インデックス付与」
「インデックス付与」は、配列の各要素に「何番目か」という情報をくっつける処理です。
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);
}
JavaScriptwithIndex の薄いラッパーですが、「これは画面表示用の番号だよ」という意図を名前で伝えられます。
ページングと組み合わせるときの注意
ページングされた一覧で「全体の何番目か」を表示したいときは、
「ページ番号」と「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引数をその場で使う」状態から、「意図のはっきりしたインデックス設計」に一段ステップアップしていきます。
