The inconsistency is because the Number::toString abstract operation is underspecified.
Your question boils down to BigInt(String(x))
≟ BigInt(x)
, which might assumed to be an equality for integer numbers x
but is in fact not.
In your particular case, for x=18446744073709551616
or x=18446744073709552000
(or anything in between, and even a bit around), the string representation of the number yields '18446744073709552000'
whereas the exact mathematical value is 18446744073709551616. (We know this because the NumberToBigInt operation is exact - it gets you the mathematical value of the number, or an error if it's not an integer).
We also find the following note on Number.prototype.toFixed
:
The output of toFixed
may be more precise than toString
for some values because toString
only prints enough significant digits to distinguish the number from adjacent Number values. For example,
(1000000000000000128).toString()
returns "1000000000000000100"
, while
(1000000000000000128).toFixed(0)
returns "1000000000000000128"
.
To answer the titular question
Why does Number(“x”) == BigInt(“x”) … only sometimes?
It's because the limited precision of floating point Number values. There are multiple numeric literals that are parsed to exactly the same number. Similar to your first example, let's take the bigint 20000000000000000n
. There is a floating point number with the same mathematical value, specifically
+1
× 0b10001110000110111100100110111111000001
× 20b10001
= 1 × 152587890625 × 217
= 20000000000000000
There are multiple integer number literals that evaluate to this Number value: 19999999999999998
, 19999999999999999
, 20000000000000000
, 20000000000000001
, and 20000000000000002
. (Notice it's not always rounding up). This is also what happens when you take these as strings and use unary +
or Number
on them.
But if you take the respective bigint literals with the same textual representation, they will evaluate to 5 different BigInt values. Only one of which will compare equal (with ==
) to, i.e. have the same mathematical value as, the Number value.
This is consistent: there's always exactly one, and it's the one that can be represented precisely as a floating point number.
Why isn't this behavior consistent?
Your confusion comes from the string representation of the Number value 18446744073709551616. When printing the number literal 18446744073709551616
, or also +'18446744073709551616'
, you got 18446744073709552000
(because the console uses String()
/.toString()
internally), which made you assume that 18446744073709552000 was its mathematical value, and that 18446744073709552000n
should compare equal to it. But it's not :-/
console.log(18446744073709551616..toString());
console.log(18446744073709551616..toFixed());
console.log(18446744073709552000..toString());
console.log(18446744073709552000..toFixed());
Which to believe?