JavaScript for impatient programmers (ES2021 edition)
Please support this book: buy it or donate
(Ad, please don’t block.)

18 Bigints – arbitrary-precision integers [ES2020] (advanced)



In this chapter, we take a look at bigints, JavaScript’s integers whose storage space grows and shrinks as needed.

18.1 Why bigints?

Before ECMAScript 2020, JavaScript handled integers as follows:

Sometimes, we need more than signed 53 bits – for example:

18.2 Bigints

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'

18.2.1 Going beyond 53 bits for integers

JavaScript numbers are internally represented as a fraction multiplied by an exponent (see §16.8 “Background: floating point precision” 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

18.2.2 Example: using bigints

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]
);

18.3 Bigint literals

Like number literals, bigint literals support several bases:

Negative bigints are produced by prefixing the unary minus operator: -0123n

18.3.1 Underscores (_) 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_00; // 123 thousand dollars

As with number literals, two restrictions apply:

18.4 Reusing number operators for bigints (overloading)

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

18.4.1 Arithmetic operators

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

18.4.2 Ordering operators

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

18.4.3 Bitwise operators

18.4.3.1 Bitwise operators for numbers

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
18.4.3.2 Bitwise operators for bigints

For bigints, bitwise operators interpret a negative sign as an infinite two’s complement – for example:

That is, a negative sign is more of an external flag and not represented as an actual bit.

18.4.3.3 Bitwise Not (~)

Bitwise Not (~) inverts all bits:

> ~0b10n
-3n
> ~0n
-1n
> ~-2n
1n
18.4.3.4 Binary bitwise operators (&, |, ^)

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'
18.4.3.5 Bitwise signed shift operators (<< 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
18.4.3.6 Bitwise unsigned right shift operator (>>>)

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.

18.4.4 Loose equality (==) and inequality (!=)

Loose equality (==) and inequality (!=) coerce values:

> 0n == false
true
> 1n == true
true

> 123n == 123
true

> 123n == '123'
true

18.4.5 Strict equality (===) and inequality (!==)

Strict equality (===) and inequality (!==) only consider values to be equal if they have the same type:

> 123n === 123
false
> 123n === 123n
true

18.5 The wrapper constructor BigInt

Analogously to numbers, bigints have the associated wrapper constructor BigInt.

18.5.1 BigInt as a constructor and as a function

Table 13: Converting values to bigints.
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())
18.5.1.1 Converting 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
18.5.1.2 Converting strings

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
18.5.1.3 Non-integer numbers produce exceptions
> BigInt(123.45)
RangeError: The number 123.45 cannot be converted to a BigInt because
it is not an integer
> BigInt(123)
123n
18.5.1.4 Converting objects

How objects are converted to bigints can be configured – for example, by overriding .valueOf():

> BigInt({valueOf() {return 123n}})
123n

18.5.2 BigInt.prototype.* methods

BigInt.prototype holds the methods “inherited” by primitive bigints:

18.5.3 BigInt.* methods

18.5.4 Casting and 64-bit integers

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);

18.6 Coercing bigints to other primitive types

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:

18.7 TypedArrays and DataView operations for 64-bit values

Thanks to bigints, Typed Arrays and DataViews can support 64-bit values:

18.8 Bigints and JSON

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

18.8.1 Stringifying bigints

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"}'
);

18.8.2 Parsing bigints

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 }
);

18.9 FAQ: Bigints

18.9.1 How do I decide when to use numbers and when to use bigints?

My recommendations:

All existing web APIs return and accept only numbers and will only upgrade to bigint on a case-by-case basis.

18.9.2 Why not just increase the precision of numbers in the same manner as is done for bigints?

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: