こんにちは!フロントエンドからNode.jsの深層まで、日夜JavaScriptと向き合っているシニアアーキテクトです。
今回は、JavaScriptにおける数値を扱う上で避けて通れない、しかし多くの開発者が一度はハマる「数値精度の限界と、それを救うBigInt型」についてお話しします。
他のプログラミング言語から来た方や、これからJavaScriptのマスターを目指す方にとって、この「数値をどうメモリ上で表現しているか」という世界を知ることは、単なる文法の暗記を超えた最高の体験になりますよ。ここをクリアすれば、JavaScriptのデータ型に対する理解はグッと深まります。一緒に紐解いていきましょう!
—
1. なぜJavaScriptの「数字」は巨大な数を正確に扱えないのか?
私たちが普段何気なく使っている `123` や `3.14` といった数値。JavaScriptでは、整数も小数もすべて `Number`型 という1つのデータ型で管理されています。
実はこの `Number`型、内部的には 「倍精度浮動小数点数(IEEE 754規格)」 という形式でメモリ(V8エンジンのヒープやスタック空間)に保持されています。これによって、小数点を含む計算を高速に行えるのですが、「大きな整数を正確に表すのが苦手」 という致命的な裏の顔を持っています。
限界の壁:安全な整数の最大値 `Number.MAX_SAFE_INTEGER`
JavaScriptの `Number` が正確に表せる限界の整数、それが `Number.MAX_SAFE_INTEGER` です。具体的な値を見てみましょう。
// Number型が安全に扱える限界値を確認してみましょう
console.log(Number.MAX_SAFE_INTEGER);
// 出力結果: 9007199254740991 (約9京!)
「約9京もあるなら十分じゃないか」と思いますよね。しかし、現代のWebアプリケーションでは、データベースの主キー(UUIDの代替としての巨大なID)、暗号学的処理、高精度な金融計算など、この9京を軽々と超える数値を扱う場面がゴロゴロ転がっています。
ここで、この限界を超えたときに何が起きるのか、実際のコードで見てみましょう。
// 限界値に「1」を足してみます
const safeMax = Number.MAX_SAFE_INTEGER;
console.log(safeMax + 1); // 出力: 9007199254740992 (ここはまだ正確)
// さらに「2」を足してみます
console.log(safeMax + 2); // 出力: 9007199254740992 ??? あれ?「1」足したときと結果が同じ!?
おや? `+ 2` をしたはずなのに、結果が `+ 1` のときと全く同じになってしまいました。
これが、いわゆる「精度落ち(桁落ち)」と呼ばれる現象です。V8エンジンは、限られたビット数(64ビット)の中に無理やり巨大な数値を収めようとするため、入りきらない細かい部分を「丸めて(切り捨てて)」しまうのです。エラーを出して止まってくれれば親切ですが、「何もエラーを出さずに、しれっと間違った値を返す」のがこの問題の恐ろしいところですね。
—
2. 救世主登場!無限の精度を持つ `BigInt` 型の基本
この「巨大な整数を正確に扱いたい」という長年の開発者たちの悲願を解決するために、ES2020で導入されたのが `BigInt`(ビッグイント)型 です。
`BigInt` は、メモリが許す限り(実質的に無限の桁数で)、どれほど巨大な整数であっても一厘の狂いもなく正確に保持・計算できるプリミティブ型です。
BigIntの作り方と基本的な使い方
`BigInt` を作る方法はとてもシンプルです。整数の末尾に `n` をつけるか、`BigInt()` 関数を使います。
// 末尾に ‘n’ をつけるだけでBigIntになります
const a = 9007199254740991n;
const b = 3n;
console.log(a + b);
// 出力: 9007199254740994n (正確に計算されています!)
// BigInt関数を使う方法
const hugeNumber = Big(“90071992547409923456789”); // ※文字列として渡すのが安全
ここで非常に重要なポイントがあります。`BigInt` はあくまで「整数」のための型です。小数点(`1.5n` のような書き方)を入れようとすると、JavaScriptは即座に構文エラー(SyntaxError)を吐き出して教えてくれます。
// ⚠️ 陥りがちな文法エラー例
const invalid = 10.5n;
// 出力: SyntaxError: Invalid or unexpected token
小数を扱いたい場合は、別の高精度演算ライブラリ(Decimal.jsなど)を検討する必要がありますが、巨大な「整数」であれば `BigInt` が最高の相棒になります。
—
3. Number型との相互運用における「致命的な罠」と注意点
「じゃあ、これからは全部 `BigInt` を使えば安心だね!」……と言いたいところですが、実務の世界はそう甘くありません。`Number`型と `BigInt`型は、どちらも数値を表すプリミティブ型でありながら、お互いを混ぜて直接計算することができません。
const num = 10;
const big = 10n;
// Number型とBigInt型を直接足そうとすると…?
console.log(num + big);
// 出力: TypeError: Cannot mix BigInt and other types, use explicit conversions
TypeError(型エラー)が発生しましたね。「混ぜるな危険」のルールが厳格に適用されます。
計算したい場合は、必ずどちらかの型に明示的に変換(キャスト)してあげる必要があります。
// Numberに合わせる場合(※ただし巨大な数値だと精度が落ちるリスクがあります)
console.log(BigInt(num) + big); // 20n
// または Number に変換する場合
console.log(num + Number(big)); // 20
🚨 実務で一番ハマる罠:JSONシリアライズの壁
API通信やローカルストレージへの保存で、JavaScriptのオブジェクトをJSON文字列に変換する(`JSON.stringify`)機会は多いと思います。ここで `BigInt` を含めると、盛大にクラッシュします。
const userData = {
id: 9007199254740992345n, // 巨大なBigIntのID
name: “Taro”
};
// JSON文字列に変換しようとすると…
try {
JSON.stringify(userData);
} catch (e) {
console.error(e.message);
// 出力: Do not know how to serialize a BigInt
}
V8エンジンの標準JSONシリアライザは、`BigInt` をどのように文字列化してよいか分からないため、容赦なくエラーを投げます。APIのレスポンス作成時などにこれでハマる開発者が後を絶ちません。
【回避策】JSONでのシリアライズ・デシリアライズのテクニック
この問題を回避するためには、`JSON.stringify` する前に手動で文字列に変換するか、カスタムのreplacer関数を挟むのが定石です。
const userData = {
id: 9007199254740992345n,
name: “Taro”
};
// 解決策1: BigIntを文字列(String)に変換してJSON化する
const jsonString = JSON.stringify(userData, (key, value) =>
typeof value === ‘bigint’ ? value.toString() : value
);
console.log(jsonString);
// 出力: {“id”:”9007199254740992345″,”name”:”Taro”}
// ※フロントエンド側で受け取った後に必要に応じて再び BigInt(“…”) に戻します
—
まとめ:ここをクリアすれば、JavaScriptの基本はバッチリ!
いかがでしたでしょうか? 今回のポイントを簡潔にまとめます。
1. `Number`型は安全な精度の限界(`Number.MAX_SAFE_INTEGER`)があるため、巨大な整数では精度低下(丸め誤差)がSilentに起きる。
2. `BigInt`型(末尾に `n`)を使えば、どれほど大きな整数でも正確に計算できる(ただし小数は扱えない)。
3. `Number` と `BigInt` は直接計算できないため、明示的な型変換が必要。
4. `JSON.stringify` は `BigInt` を直接シリアライズできないため、`.toString()` で文字列に変換してやり取りするのが実務の定番パターン。
数値の裏側にあるメモリの仕組みと型の制約を理解していると、バグを踏んだときにも「あ、これはあそこの仕様だな」と瞬時に脳内トレースできるようになります。
ここをしっかりと押さえたあなたなら、もうJavaScriptのデータ型で迷うことはありません。自信を持って、よりモダンで堅牢なコードを書いていきましょう!