お疲れ。フロントエンドの現場で日々、JavaScriptの不可思議な挙動と格闘している君なら、一度は「あれ?」と首を傾げたことがあるはずだ。
「なぜ、JavaScriptはここまで柔軟に型の壁をぶち壊して暗黙の型変換をしてくれるのに、BigIntとNumberを混ぜると容赦なくエラーを吐くのか?」ってね。
`10n + 5` と書いた瞬間に、V8エンジン(Chromeの裏側であくせく動いているアイツだ)から `TypeError: Cannot mix BigInt and other types` という冷徹な宣告を受ける。あの赤字のエラーログを見た時の切なさは、実務をこなすエンジニアなら誰もが一度は通る道だ。
今日は、なぜこの厳格な制約がもうけられているのか、ブラウザの裏側で何が起きているのか、そして俺たちが実務でこの仕様とどう向き合い、スマートに乗りこなすべきかについて、シニアの視点から徹底的に叩き込んでやろう。
—
なぜBigIntとNumberは「混ぜるな危険」なのか?
JavaScriptの長所であり、同時に諸刃の剣でもあるのが「暗黙の型変換(Coercion)」だ。`”5″ + 3` と書けば `”53″` になり、`”5″ – 3` と書けば `2` になる。あの手この手で型を揃えて、なんとか式を成立させようとするお節介な言語仕様だ。
しかし、BigIntとNumberの混在に関しては、このお節介機能が完全に封印されている。 理由はシンプルかつ絶対的で、「精度の不可逆的な損失(Loss of Precision)」を防ぐためだ。
浮動小数点数(Number)と巨大整数(BigInt)の絶望的なすれ違い
JavaScriptの標準的な数値型である `Number` は、IEEE 754規格に基づく倍精度浮動小数点数だ。これは「小数点をどこに置くか」を動的に管理する仕組みで、扱える安全な整数の上限は `Number.MAX_SAFE_INTEGER`(すなわち $2^{53} – 1$、約9京)まで。これを超えると、メモリ上のビットが足りなくなって丸め誤差が生じ、データの正確性が失われる。
一方、`BigInt` は、メモリが許す限り無限の桁数の整数を正確に保持するために生まれた。
もし、この2つを暗黙的に混ぜることが許されたらどうなるか?
例えば、$2^{60}$ という途方もなく大きなBigIntに、適当なNumberを足し算しようとしたとする。V8エンジンが裏側で勝手にNumberをBigIntに変換するにしても、その逆にするにしても、下位のビットが切り捨てられたり、意図しない丸めが発生してデータの整合性が音を立てて崩壊する。
「勝手に推測してデータを壊すくらいなら、ここで盛大にエラーを吐いてエンジニアに明示的な判断を促そう」というのが、TC39(JavaScriptの仕様を決めるところ)のエンジニアたちの賢明な判断だったというわけだ。
—
現場で頻出!「うっかり混在」してしまうパターンの罠
実務のフロントエンド開発で、この制約に一番引っかかりやすいのが「APIレスポンスのID」と「ユーザーの入力値・計算値」が交差する瞬間だ。
例えば、バックエンドのデータベースで採番された超巨大なID(雪の結晶のようにユニークなsnowflake IDなど)が `BigInt` として扱われており、フロントエンド側で保持している画面上のページネーションのオフセット(普通の `Number`)と足し合わせようとしたとき、例の `TypeError` が爆誕する。
ダメなコードの典型例
// バックエンドから受け取った巨大なID(BigInt)
const hugeUserId = 9007199254740993n;
// フロントエンドのUI側で持っているページネーションのオフセット(Number)
const pageOffset = 10;
try {
// ここで暗黙の型変換を期待して足し算をすると…
const nextId = hugeUserId + pageOffset;
console.log(nextId);
} catch (error) {
console.error(“ほら見たことか!:”, error.message);
// 出力: TypeError: Cannot mix BigInt and other types
}
このエラーを回避するためには、開発者自身がどちらの型に寄せるべきかを明示的にコードで指示(明示的型変換)してやらなければならない。
—
実務で使える!明示的変換のベストプラクティス
BigIntとNumberを演算させたい場合、アプローチは基本原理に基づいて2つに分かれる。
1. BigIntをNumberにキャストする(※精度落ちのリスクを許容できる場合のみ!)
2. NumberをBigIntにキャストする(※小数が含まれていない、または切り捨てても問題ない場合)
それぞれの現場での使い分けを見ていこう。
パターンA:UIのカウンターや表示用で、精度落ちを気にしなくてよい場合(Big -> Number)
もし `BigInt` の値が `Number.MAX_SAFE_INTEGER` の範囲内に収まることが確実、もしくは厳密な正確性よりも「ざっくりとした数値として扱いたい」場合は、`Number()` でラップしてNumberに統一する。
const bigCount = 100n;
const normalCount = 50;
// BigIntをNumberに明示変換してから計算
const total = Number(bigCount) + normalCount;
console.log(total); // 150 (Number型として安全に計算される)
パターンB:IDや金融系など、1ビットの狂いも許されない厳密な計算の場合(Number -> Big)
こちらのほうが実務では圧倒的に重要だ。UI上の小さなインデックスやオフセットを、巨大なBigIntのベース値に加算・比較したい場合は、Number側を `BigInt()` で囲んで型を合わせる。
// 基準となる巨大なベースID(BigInt)
const baseId = 9007199254740991n;
// 画面側で操作しているインデックス(Number)
const currentIndex = 5;
// NumberをBigIntに明示変換して演算する
// ※ BigInt同士の演算はエラーにならない
const targetId = baseId + BigInt(currentIndex);
console.log(targetId); // 9007199254740996n (型が一致し、精度も完全に維持される)
⚠️ 注意すべきダークパターン:比較演算子(``)の罠
ここで一つ、君たちに重要な注意点がある。
算術演算子(`+`, `-`, “, `/` など)ではBigIntとNumberの混在は問答無用でエラーになるが、比較演算子( `<`, `>`, `===`, `==` )ではエラーにならず、値として評価される。
console.log(10n > 5); // true (これは直感的で問題ない)
console.log(10n === 10); // false (型が違うので当然false)
一見すると「お、比較は緩くて便利じゃん」と思うかもしれないが、これがバグの温床になる。
特に `===` は型まで厳格に見るため、`10n === 10` は `false` を返す。APIから飛んできた数値が `Number` なのか `BigInt` なのかを意識せずに条件分岐を書いていると、この「値は同じなのに型違いで一致しない」という幽霊バグに何時間も悩まされることになる。
実務でIDや数値を比較する際は、比較する前に必ずどちらかの型に揃える(Normalization)関数を一枚挟むのが、シニアとしてのプロの流儀だ。
—
チーフアーキテクトからの実践的アドバイス
最後に、大規模なフロントエンド・アーキテクチャを設計する際の心得を伝えておく。
1. API境界で型をバインドしろ
バックエンドから来るデータが `JSON.parse()` を経由する場合、JSONの仕様上 `BigInt` は存在しないため、巨大な数値は文字列(`string`)として飛んでくるか、精度が落ちた `Number` として届くはずだ。
もしAPIレイヤーで巨大IDを扱うなら、受け取った瞬間に `BigInt(response.id)` に変換し、アプリ内では徹底して `BigInt` として扱うか、あるいは文字列のままUIまで通すかを明確に決めろ。途中で `Number` と `BigInt` をちゃんぽんにするのだけは絶対に避けるんだ。
2. 「動くからいいや」の暗黙の変換を信じるな
JavaScriptの「うまいことやってくれる精神」は素晴らしいが、BigInt周辺の仕様に関しては、言語側が「そこは俺たちも責任持てないから、お前が明示的に型を合わせろ」と匙を投げている領域だ。
型の制約は、足枷ではなく、君のコードをバグから守るための防壁だ。仕様を正しく理解し、型を意図的にコントロールできるエンジニアになろう。
さあ、エディタに戻って、プロジェクト内の危険な混在コードがないか確認しにいこうか。

コメント