In this chapter, we take a look at bigints, JavaScript’s integers whose storage space grows and shrinks as needed.
Before ECMAScript 2020, JavaScript handled integers as follows:
There only was a single type for floating point numbers and integers: 64-bit floating point numbers (IEEE 754 double precision).
Under the hood, most JavaScript engines transparently supported integers: If a number has no decimal digits and is within a certain range, it can internally be stored as a genuine integer. This representation is called small integer and usually fits into 32 bits. For example, the range of small integers on the 64-bit version of the V8 engine is from −231 to 231−1 (source).
JavaScript numbers could also represent integers beyond the small integer range, as floating point numbers. Here, the safe range is plus/minus 53 bits. For more information on this topic, see “Safe integers” (§18.9.4).
Sometimes, we need more than signed 53 bits – for example:
Bigint is a primitive data type for integers. Bigints don’t have a fixed storage size in bits; their sizes adapt to the integers they represent:
A bigint literal is a sequence of one or more digits, suffixed with an n
– for example:
123n
Operators such as -
and *
are overloaded and work with bigints:
> 123n * 456n
56088n
Bigints are primitive values. typeof
returns a distinct result for them:
> typeof 123n
'bigint'
JavaScript numbers are internally represented as a fraction multiplied by an exponent (see “Background: floating point precision” (§18.8) for details). As a consequence, if we go beyond the highest safe integer 253−1, there are still some integers that can be represented, but with gaps between them:
> 2**53 // can be represented but same as next number
9007199254740992
> 2**53 + 1 // wrong
9007199254740992
Bigints enable us to go beyond 53 bits:
> 2n**53n
9007199254740992n
> 2n**53n + 1n
9007199254740993n
This is what using bigints looks like (code based on an example in the proposal):
/**
* Takes a bigint as an argument and returns a bigint
*/
function nthPrime(nth) {
if (typeof nth !== 'bigint') {
throw new TypeError();
}
function isPrime(p) {
for (let i = 2n; i < p; i++) {
if (p % i === 0n) return false;
}
return true;
}
for (let i = 2n; ; i++) {
if (isPrime(i)) {
if (--nth === 0n) return i;
}
}
}
assert.deepEqual(
[1n, 2n, 3n, 4n, 5n].map(nth => nthPrime(nth)),
[2n, 3n, 5n, 7n, 11n]
);
Like number literals, bigint literals support several bases:
123n
0xFFn
0b1101n
0o777n
Negative bigints are produced by prefixing the unary minus operator: -0123n
_
) as separators in bigint literals ES2021Just like in number literals, we can use underscores (_
) as separators in bigint literals:
const massOfEarthInKg = 6_000_000_000_000_000_000_000_000n;
Bigints are often used to represent money in the financial technical sector. Separators can help here, too:
const priceInCents = 123_000_00n; // 123 thousand dollars
As with number literals, two restrictions apply:
With most operators, we are not allowed to mix bigints and numbers. If we do, exceptions are thrown:
> 2n + 1
TypeError: Cannot mix BigInt and other types, use explicit conversions
The reason for this rule is that there is no general way of coercing a number and a bigint to a common type: numbers can’t represent bigints beyond 53 bits, bigints can’t represent fractions. Therefore, the exceptions warn us about typos that may lead to unexpected results.
Consider the following expression:
2**53 + 1n
Should the result be 9007199254740993n
or 9007199254740992
?
It is also not clear what the result of the following expression should be:
2n**53n * 3.3
Binary +
, binary -
, *
, **
work as expected:
> 7n * 3n
21n
It is OK to mix bigints and strings:
> 6n + ' apples'
'6 apples'
/
and %
round towards zero by removing the fraction (like Math.trunc()
):
> 1n / 2n
0n
Unary -
works as expected:
> -(-64n)
64n
Unary +
is not supported for bigints because much code relies on it coercing its operand to number:
> +23n
TypeError: Cannot convert a BigInt value to a number
==
) and inequality (!=
)Loose equality (==
) and inequality (!=
) coerce values:
> 0n == false
true
> 1n == true
true
> 123n == 123
true
> 123n == '123'
true
===
) and inequality (!==
)Strict equality (===
) and inequality (!==
) only consider values to be equal if they have the same type:
> 123n === 123
false
> 123n === 123n
true
Ordering operators <
, >
, >=
, <=
work as expected:
> 17n <= 17n
true
> 3n > -1n
true
Comparing bigints and numbers does not pose any risks. Therefore, we can mix bigints and numbers:
> 3n > -1
true
Exercise: Converting numbers-based code to bigints
exercises/bigints/gcd-bigint_test.mjs
Bitwise operators interpret numbers as 32-bit integers. These integers are either unsigned or signed. If they are signed, the negative of an integer is its two’s complement: If we add an integer to its two’s complement and ignore overflow (digits beyond 32 bits) then the result is zero.
> 2**32 - 1 >> 0 // 0b11111111111111111111111111111111
-1
If we add 1
to the binary number consisting of 32 ones, we get a one followed by 32 zeros. Everything beyond 32 bits is overflow, which means that that number is zero.
We used signed right shift operator (>>
): We shifted the left operand by zero bits, which converted it to Int32 (which is signed) and back to number.
Due to these integers having a fixed size, their highest bits indicate their signs:
> 2**31 >> 0 // highest bit is 1
-2147483648
> 2**31 - 1 >> 0 // highest bit is 0
2147483647
For bigints, bitwise operators interpret a negative sign as an infinite two’s complement – for example:
-1
is ···111111
(ones extend infinitely to the left)
-2
is ···111110
-3
is ···111101
-4
is ···111100
That is, a negative sign is more of an external flag and not represented as an actual bit.
~
)Bitwise Not (~
) inverts all bits:
assert.equal(
~0b10n,
-3n // ···111101
);
assert.equal(
~-2n, // ···111110
1n
);
&
, |
, ^
)Applying binary bitwise operators to bigints works analogously to applying them to numbers:
> (0b1010n | 0b0111n).toString(2)
'1111'
> (0b1010n & 0b0111n).toString(2)
'10'
> (0b1010n | -1n).toString(2)
'-1'
> (0b1010n & -1n).toString(2)
'1010'
<<
and >>
)The signed shift operators for bigints preserve the sign of a number:
> 2n << 1n
4n
> -2n << 1n
-4n
> 2n >> 1n
1n
> -2n >> 1n
-1n
Recall that -1n
is a sequence of ones that extends infinitely to the left. That’s why shifting it left doesn’t change it:
> -1n >> 20n
-1n
>>>
)There is no unsigned right shift operator for bigints:
> 2n >>> 1n
TypeError: BigInts have no unsigned right shift, use >> instead
Why? The idea behind unsigned right shifting is that a zero is shifted in “from the left”. In other words, the assumption is that there is a finite amount of binary digits.
However, with negative bigints (especially negative ones), there is no “left”; their binary digits extend infinitely.
Signed right shift works even with an infinite number of digits because the highest digit is preserved. Therefore, it can be adapted to bigints.
Exercise: Implementing a bit set via bigints
exercises/bigints/bit-set_test.mjs
BigInt
Analogously to numbers, bigints have the associated wrapper constructor BigInt
.
BigInt
as a constructor and as a functionnew BigInt()
: throws a TypeError
.
BigInt(x)
converts arbitrary values x
to bigint. This works similarly to Number()
, with several differences which are summarized in table 20.1 and explained in more detail in the following subsections.
x | BigInt(x) |
---|---|
undefined | Throws TypeError |
null | Throws TypeError |
boolean | false → 0n , true → 1n |
number | Example: 123 → 123n |
Non-integer → throws RangeError |
|
bigint | x (no change) |
string | Example: '123' → 123n |
Unparsable → throws SyntaxError |
|
symbol | Throws TypeError |
object | Configurable (e.g. via .valueOf() ) |
Table 20.1: Converting values to bigints.
undefined
and null
A TypeError
is thrown if x
is either undefined
or null
:
> BigInt(undefined)
TypeError: Cannot convert undefined to a BigInt
> BigInt(null)
TypeError: Cannot convert null to a BigInt
If a string does not represent an integer, BigInt()
throws a SyntaxError
(whereas Number()
returns the error value NaN
):
> BigInt('abc')
SyntaxError: Cannot convert abc to a BigInt
The suffix 'n'
is not allowed:
> BigInt('123n')
SyntaxError: Cannot convert 123n to a BigInt
All bases of bigint literals are allowed:
> BigInt('123')
123n
> BigInt('0xFF')
255n
> BigInt('0b1101')
13n
> BigInt('0o777')
511n
> BigInt(123.45)
RangeError: The number 123.45 cannot be converted to a BigInt because
it is not an integer
> BigInt(123)
123n
How objects are converted to bigints can be configured – for example, by overriding .valueOf()
:
> BigInt({valueOf() {return 123n}})
123n
BigInt.prototype.*
methodsBigInt.prototype
holds the methods “inherited” by primitive bigints:
BigInt.prototype.toLocaleString(locales?, options?)
BigInt.prototype.toString(radix?)
BigInt.prototype.valueOf()
BigInt.*
methods: castingBigInt.asIntN(width, theInt)
Casts theInt
to width
bits (signed). This influences how the value is represented internally.
BigInt.asUintN(width, theInt)
Casts theInt
to width
bits (unsigned).
Casting allows us to create integer values with a specific number of bits – e.g., if we want to restrict ourselves to 64-bit integers, we always have to cast:
const uint64a = BigInt.asUintN(64, 12345n);
const uint64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, uint64a * uint64b);
Exercise: Implementing the analog of
Number.parseInt()
for bigints
exercises/bigints/parse-bigint_test.mjs
This table show what happens if we convert bigints to other primitive types:
Convert to | Explicit conversion | Coercion (implicit conversion) |
---|---|---|
boolean | Boolean(0n) → false | !0n → true |
Boolean(int) → true | !int → false |
|
number | Number(7n) → 7 (example) | +int → TypeError (1) |
string | String(7n) → '7' (example) | ''+7n → '7' (example) |
Footnote:
+
is not supported for bigints, because much code relies on it coercing its operand to number.
Thanks to bigints, Typed Arrays and DataViews can support 64-bit values:
BigInt64Array
BigUint64Array
DataView.prototype.getBigInt64()
DataView.prototype.setBigInt64()
DataView.prototype.getBigUint64()
DataView.prototype.setBigUint64()
The JSON standard is fixed and won’t change. The upside is that old JSON parsing code will never be outdated. The downside is that JSON can’t be extended to contain bigints.
Stringifying bigints throws exceptions:
> JSON.stringify(123n)
TypeError: Do not know how to serialize a BigInt
> JSON.stringify([123n])
TypeError: Do not know how to serialize a BigInt
Therefore, our best option is to store bigints in strings:
const bigintPrefix = '[[bigint]]';
function bigintReplacer(_key, value) {
if (typeof value === 'bigint') {
return bigintPrefix + value;
}
return value;
}
const data = { value: 9007199254740993n };
assert.equal(
JSON.stringify(data, bigintReplacer),
'{"value":"[[bigint]]9007199254740993"}'
);
The following code shows how to parse strings such as the one that we have produced in the previous example.
function bigintReviver(_key, value) {
if (typeof value === 'string' && value.startsWith(bigintPrefix)) {
return BigInt(value.slice(bigintPrefix.length));
}
return value;
}
const str = '{"value":"[[bigint]]9007199254740993"}';
assert.deepEqual(
JSON.parse(str, bigintReviver),
{ value: 9007199254740993n }
);
My recommendations:
Array.prototype.forEach()
Array.prototype.entries()
All existing web APIs return and accept only numbers and will only upgrade to bigint on a case-by-case basis.
One could conceivably split number
into integer
and double
, but that would add many new complexities to the language (several integer-only operators etc.). I’ve sketched the consequences in a Gist.
Acknowledgements: