# BigIntの配列をsortすると失敗する？数値順の比較と元配列を変えない並べ替え

BigInt配列が文字列順になる原因と、a-bを比較関数に使うと例外になる理由を実行例で解説します。精度を落とさない比較、toSorted、欠損値、同順位の扱いまで確認します。

正規URL: https://3mikan.com/archives/6082

著者: みかん
公開: 2026-09-06T00:00:00.000Z
更新: 2026-09-06T00:00:00.000Z

ブロック番号や大きな整数を`BigInt`の配列へ入れ、`sort()`したのに数値順にならない。いつもの`(a, b) => a - b`へ変えると、今度は`TypeError`になる。この問題では、保存している値の型と、比較関数が返す値の型を分けて考えます。

この記事では、普通のJavaScript配列を対象に、精度を落とさない比較と、元配列を変えない並べ替えを作ります。続いて、値の欠損と同順位の扱いを確認します。扱う番号やログはすべて説明用で、外部RPCやアカウントは使いません。

掲載例はNode.js 22以降を使い、この記事のJavaScriptを上から順に同じ`example.mjs`へ保存して、`node example.mjs`で実行できます。テキスト形式で示す出力や表は、ファイルへ含めません。

## 引数なしのsortは、BigIntでも数値順ではない

まず、次の3要素を並べます。

```js
console.log([3n, 20n, 100n].sort());
// [ 100n, 20n, 3n ]
```

期待した数値順は`3, 20, 100`ですが、実行結果は逆です。通常の`Array`で比較関数を省略すると、値を文字列として比べるためです。今回の入力では`"100"`、`"20"`、`"3"`という順に並びます。[^sort]

元の要素が文字列へ書き換わるわけではありません。並べ終わっても要素は`BigInt`ですが、並べる基準が数値の大小になっていません。

なお、ここで説明しているのは普通の`Array`です。`BigInt64Array`などの型付き配列は別のメソッド定義を持つため、同じ省略時の規則だと広げないでください。[^sort]

## a-bは計算できても、その戻り値をsortが使えない

次の比較も、よくある修正です。

```js
try {
  [3n, 1n].sort((a, b) => a - b);
} catch (error) {
  console.log(error.name);
}
// TypeError
```

`3n - 1n`という引き算自体は有効で、結果は`2n`です。しかし、配列の比較処理は比較関数の戻り値に数値変換を行い、`BigInt`の戻り値では例外になります。**比較対象がBigIntでも、比較結果までBigIntにする必要はありません。**[^compare]

並べ替えに必要なのは、差の正確な大きさではなく、「前へ」「同順位」「後ろへ」という3通りです。

| 比較関数が返す値 | 意味 |
| --- | --- |
| 負の`Number` | `a`を`b`より前へ |
| `0` | この比較では同順位 |
| 正の`Number` | `a`を`b`より後ろへ |

そこで、次のように大小だけを返します。

```js
function compareBigInts(a, b) {
  if (typeof a !== 'bigint' || typeof b !== 'bigint') {
    throw new TypeError('both values must be bigint');
  }
  return a < b ? -1 : a > b ? 1 : 0;
}

console.log([3n, 20n, 100n].sort(compareBigInts));
// [ 3n, 20n, 100n ]
```

最後の`-1`、`1`、`0`には、`n`を付けません。元の大きな整数を保ったまま大小を調べ、比較結果だけを小さな`Number`として返しています。

`(a, b) => a > b`のように真偽値だけを返す方法も避けます。小さい場合に負の値を返さず、同順位との区別が付かないため、ここで必要な比較規則を満たしません。

## Numberへ変えてから比べると、隣の整数が同じになる

例外を消すために、比較する二つの値をそれぞれ`Number`へ変えてはいけません。次の2つは異なる整数ですが、この変換では区別を失います。[^number]

```js
const bigger = 9_007_199_254_740_993n;
const smaller = 9_007_199_254_740_992n;

console.log(Number(bigger) === Number(smaller));
// true

console.log([bigger, smaller].sort((a, b) => Number(a) - Number(b)));
// [ 9007199254740993n, 9007199254740992n ]

console.log([bigger, smaller].sort(compareBigInts));
// [ 9007199254740992n, 9007199254740993n ]
```

真ん中の式では比較結果が0となり、この入力順が残っています。例外が出ないことと、数値順が正しいことは別です。

ここで問題にしているのは`Number(a) - Number(b)`です。`Number(a - b)`は引き算の時点ではBigIntを保つため、同じ理由で壊れると説明してはいけません。ただし、大小だけで足りる処理なら、大きな差を作らない`compareBigInts`のほうが意図を直接表せます。

## 元の配列を残すならtoSortedを使う

`sort()`は元配列の順序を書き換えます。別の変数へ返り値を入れても、元配列を残すコピーにはなりません。一方、`toSorted()`は新しい配列を作ります。[^sort] [^copy]

```js
const original = [3n, 1n, 2n];
const sorted = original.toSorted(compareBigInts);

console.log(original); // [ 3n, 1n, 2n ]
console.log(sorted);   // [ 1n, 2n, 3n ]
console.log(original === sorted); // false
```

`toSorted`を使わない環境では、今回のような要素が詰まった普通の配列なら、`[...original].sort(compareBigInts)`で先に配列を複製する方法もあります。

ただし、新しくなるのは配列の入れ物です。要素がオブジェクトなら、中身のオブジェクトを深く複製する操作ではありません。以下はその違いを確かめる例です。

```js
const records = [{ name: 'A', value: 2n }, { name: 'B', value: 1n }];
const ordered = records.toSorted(
  (a, b) => compareBigInts(a.value, b.value)
);

ordered[0].name = 'changed';
console.log(records[1].name); // changed
```

「並べ替えても元の順序は残る」と「結果の中身を編集しても元のデータは変わらない」を分けます。後者も必要なら、並べ替えとは別に、どこまで複製するかを決めます。

## 入力検査を比較関数だけに任せない

比較関数に型検査を書けば、配列全体の検証になるとは限りません。空配列や要素が一つの配列では比較が発生しないことがあり、`undefined`には比較関数を呼ばない扱いもあります。[^compare]

そのため、「すべての要素がBigIntである」という入力契約は、並べ替える前に検査します。

```js
function sortBigInts(values) {
  if (!Array.isArray(values)) {
    throw new TypeError('values must be an array');
  }
  for (let index = 0; index < values.length; index += 1) {
    if (!Object.hasOwn(values, index) || typeof values[index] !== 'bigint') {
      throw new TypeError(`values[${index}] must be a bigint element`);
    }
  }
  return values.toSorted(compareBigInts);
}
```

この関数は、空配列は認めますが、途中の要素がない配列、`null`、文字列、`Number`との混在は拒否します。これはこの記事で採用した入力方針であり、JavaScriptがそれらの並べ替えをすべて禁止しているという意味ではありません。

```js
console.log(sortBigInts([5n, -1n, 0n, 5n]));
// [ -1n, 0n, 5n, 5n ]

try {
  sortBigInts([1n, undefined]);
} catch (error) {
  console.log(error.name);
}
// TypeError
```

`Object.hasOwn`の確認は、配列の長さだけ存在して実際の要素が抜けている箇所も拒否するためです。欠損を自動でゼロへ変えて並べると、「データがない」と「値が0」を見分けられなくなります。

## 同順位をどう並べるかは、別の規則として決める

安定した並べ替えでは、比較結果が0になる要素同士は入力時の順序を保ちます。しかし、これは「正しい取引順へ復元される」という意味ではありません。入力の到着順が変われば、同順位の並びも変わります。[^sort]

たとえば、同一チェーンの採用した履歴に属するログを扱うなら、ブロック番号だけでなく、ブロック内でのログ位置も比較できます。RPCには`blockNumber`と`logIndex`という項目があります。[^rpc]

次は、その2項目を検証・整数化済みとした合成例です。

```js
const logs = [
  { id: 'C', blockNumber: 11n, logIndex: 0n },
  { id: 'B', blockNumber: 10n, logIndex: 2n },
  { id: 'A', blockNumber: 10n, logIndex: 0n },
];

const orderedLogs = logs.toSorted((a, b) =>
  compareBigInts(a.blockNumber, b.blockNumber) ||
  compareBigInts(a.logIndex, b.logIndex)
);

console.log(orderedLogs.map(log => log.id));
// [ 'A', 'B', 'C' ]
```

最初の比較が0のときだけ、2番目の比較へ進みます。ブロック番号とログ位置が両方とも同じなら、このコードでは入力順を維持します。

異なるチェーンや、同じ高さの別ブロックを混ぜてよいという意味ではありません。ブロックハッシュや削除されたログの扱いは、並べ替えより前に決める必要があります。順序を整える関数だけで、採用すべき履歴の正しさまでは判定しません。

## まとめ：元の整数は保ち、比較結果だけ小さく返す

BigInt配列を数値順へ並べるなら、大小を`BigInt`のまま比較し、`-1`・`0`・`1`を`Number`として返します。元配列を残す場合は`toSorted`を使い、要素の型と欠損は比較関数の外で検査します。

最後に、同順位の扱いを決めます。計算の精度、配列の変更、同順位の基準は別の論点です。ログ自体の項目や意味を確認するときは、[ABIとイベントログの読み方](/archives/6005)を参照してください。

## 実行環境と出典

コードはNode.js v22.16.0で2026-09-06に確認しました。小さな整数、負数、重複、`Number`の安全な整数範囲を超える隣接値、欠損、元配列の保持を確認しています。RPCから取得した実ログの並べ替えや速度比較は行っていません。

[^sort]: [ECMAScript 2025：Array.prototype.sortとSortIndexedProperties](https://tc39.es/ecma262/2025/multipage/indexed-collections.html#sec-array.prototype.sort)。通常配列、安定性、比較規則を確認。確認日：2026-09-06。
[^compare]: [ECMAScript 2025：CompareArrayElements](https://tc39.es/ecma262/2025/multipage/indexed-collections.html#sec-comparearrayelements)。比較結果の数値変換と`undefined`の扱い。確認日：2026-09-06。
[^copy]: [ECMAScript 2025：Array.prototype.toSorted](https://tc39.es/ecma262/2025/multipage/indexed-collections.html#sec-array.prototype.tosorted)。確認日：2026-09-06。
[^number]: [ECMAScript 2025：Number.MAX_SAFE_INTEGER](https://tc39.es/ecma262/2025/multipage/numbers-and-dates.html#sec-number.max_safe_integer)。確認日：2026-09-06。
[^rpc]: [Ethereum JSON-RPC API：eth_getLogs](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getlogs)。確認日：2026-09-06。

## 確認した一次情報

- [ECMAScript 2025: Array.prototype.sort](<https://tc39.es/ecma262/2025/multipage/indexed-collections.html#sec-array.prototype.sort>): 確認日 2026-09-06
- [ECMAScript 2025: CompareArrayElements](<https://tc39.es/ecma262/2025/multipage/indexed-collections.html#sec-comparearrayelements>): 確認日 2026-09-06
- [ECMAScript 2025: Array.prototype.toSorted](<https://tc39.es/ecma262/2025/multipage/indexed-collections.html#sec-array.prototype.tosorted>): 確認日 2026-09-06
- [ECMAScript 2025: Number.MAX\_SAFE\_INTEGER](<https://tc39.es/ecma262/2025/multipage/numbers-and-dates.html#sec-number.max_safe_integer>): 確認日 2026-09-06
- [Ethereum JSON-RPC: log blockNumber and logIndex](<https://ethereum.org/developers/docs/apis/json-rpc/>): 確認日 2026-09-06
