The language has one Number type: the spec defines it as an IEEE 754 double-precision value. Smi and HeapNumber are V8's internal representations (observed on Node 24.19, V8 13.6, 64-bit arm64); you cannot see them from JavaScript semantics, only from performance and debug tooling.
Tagging. V8 values are tagged words. Per the pointer-compression post, the least significant bit(s) distinguish a heap-object pointer from a small integer (Smi). A Smi carries its value in the word itself, so it needs no heap allocation and no pointer to follow.
Range. The post says a Smi has a 32-bit payload on 64-bit builds and 31 bits when pointer compression is on, because compression makes the tagged word 32 bits wide. On this Node, process.config.variables.v8_enable_pointer_compression is 0, and %IsSmi was true for 2**31 - 1 and -(2**31) and false one step beyond either. So the range here is the 32-bit one. A browser such as Chrome ships with compression on, per the post, so I would expect 31 bits there; I did not test it.
HeapNumber. Any number that does not fit a Smi is a heap object holding a boxed double: fractions, -0, NaN, Infinity, and integers outside the Smi range. In %DebugPrint, 1.5 is a [HeapNumber] with instance size: 16 here.
Consequences.
- Whether a number is a Smi depends on its value, not on how you wrote it:
1.0and10 / 2are Smis,7 / 2is not. - Arrays hold doubles unboxed in
PACKED_DOUBLE_ELEMENTS(elements kinds), so a double array does not need one HeapNumber per element. - Optimized code that expects Smis deoptimizes when it meets a HeapNumber (
not a Smi) or an overflowing result (overflow) (deoptimization).