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.3).
Sometimes, we need more than signed 53 bits – for example:
Bigint is a new 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 new 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 - 2 // safe
9007199254740990
> 2**53 - 1 // safe
9007199254740991
> 2**53 // unsafe, same as next integer
9007199254740992
> 2**53 + 1
9007199254740992
> 2**53 + 2
9007199254740994
> 2**53 + 3
9007199254740996
> 2**53 + 4
9007199254740996
> 2**53 + 5
9007199254740996
Bigints enable us to go beyond 53 bits:
> 2n**53n
9007199254740992n
> 2n**53n + 1n
9007199254740993n
> 2n**53n + 2n
9007199254740994n
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 [ES2021]Just 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.
For example, should the result of the following expression be 9007199254740993n
or 9007199254740992
?
2**53 + 1n
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'
/
, %
round towards zero (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
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
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 (adding an integer to its two’s complement – while ignoring overflow – produces zero):
> 2**32-1 >> 0
-1
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:
> ~0b10n
-3n
> ~0n
-1n
> ~-2n
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 bigints, there is no “left”, their binary digits extend infinitely. This is especially important with negative numbers.
Signed right shift works even with an infinite number of digits because the highest digit is preserved. Therefore, it can be adapted to bigints.
==
) 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
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.*
methodsBigInt.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. If we want to restrict ourselves to just 64-bit integers, we have to always cast:
const uint64a = BigInt.asUintN(64, 12345n);
const uint64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, uint64a * uint64b);
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: