JavaScript Bitwise Operators with Non-Integers

In JavaScript, bitwise operators do not operate directly on floating-point numbers, strings, or complex objects. Instead, the JavaScript engine implicitly coerces any non-integer operand into a 32-bit integer before performing the operation. This article explains the internal conversion process, demonstrates how different non-integer data types behave under bitwise operations, and highlights common edge cases.

The Conversion Pipeline: ToInt32 and ToUint32

JavaScript numbers are stored as 64-bit floating-point values (IEEE 754). When a bitwise operator is applied, the engine performs the internal ToInt32 abstract operation on the operands (or ToUint32 in the case of the zero-fill right shift operator, >>>).

This conversion involves two steps: 1. Type Coercion (ToNumber): The operand is converted to a numeric primitive. 2. Integer Truncation (ToInt32): The numeric value is clamped modulo \(2^{32}\) into a signed 32-bit integer ranging from -2,147,483,648 to 2,147,483,647.

Behavior with Floating-Point Numbers

When applied to floating-point numbers, bitwise operators discard the fractional portion entirely. The operation truncates toward zero rather than rounding to the nearest whole integer.

// Positive floats truncate downward
console.log(5.9 | 0); // 5

// Negative floats truncate upward
console.log(-5.9 | 0); // -5

// Bitwise NOT with decimals
console.log(~3.7); // -4 (same as ~3)

Because of this behavior, idioms like x | 0 or ~~x are frequently used in JavaScript as shorthand to truncate floating-point numbers into 32-bit integers.

Behavior with Special Numeric Values

Special floating-point values that cannot be represented as standard numbers are mapped directly to 0:

console.log(NaN | 0);       // 0
console.log(Infinity >> 2); // 0
console.log(~(-Infinity));  // -1 (since ~0 is -1)

Behavior with Non-Numeric Data Types

Non-numeric types first undergo standard JavaScript type coercion into numbers before the bitwise operation takes place.

Booleans

Booleans are converted to 1 (true) or 0 (false):

console.log(true << 2);  // 4  (1 << 2)
console.log(false | 8);  // 8  (0 | 8)

Strings

Numeric strings are parsed into numbers and then truncated. Non-numeric strings evaluate to NaN, which becomes 0:

console.log("42.8" | 0);    // 42
console.log("10" ^ 3);      // 9
console.log("hello" & 255); // 0 ("hello" becomes NaN, then 0)
console.log("" | 0);        // 0 (empty string becomes 0)

Null and Undefined

console.log(null | 0);      // 0
console.log(undefined | 0); // 0

Objects and Arrays

Objects and arrays are converted using their primitive conversion methods (valueOf or toString): * [] becomes "", which converts to 0. * [5.5] becomes "5.5", which converts to 5. * Plain objects {} convert to "[object Object]", which becomes NaN and then 0.

console.log([] | 0);     // 0
console.log([9.9] | 0);  // 9
console.log({} | 0);     // 0

Integer Overflow Beyond 32 Bits

Numbers exceeding the 32-bit signed integer range wrap around using two’s complement representation:

// 2^31 is 2147483648, which overflows a signed 32-bit integer
console.log(2147483648 | 0); // -2147483648

// Numbers larger than 2^32 wrap around completely
console.log(4294967296 | 0); // 0 (4294967296 is 2^32)

BigInt Exception

The BigInt type does not follow the ToInt32 truncation rules. Bitwise operations on BigInt values support arbitrary precision, but standard numbers and BigInt values cannot be mixed. Attempting to apply a bitwise operator between a BigInt and any other type results in a TypeError.

console.log(5n & 3n); // 1n
// console.log(5n & 3); // TypeError: Cannot mix BigInt and other types