Bit operations — holding flags in bits
File permissions 755, TCP flags, regex options. All of these pack multiple booleans into a single integer. Bit operations are what pack and unpack them.
You don't write them every day in typical web development, but they always come up when you read existing code or a protocol spec.
What you'll learn in this chapter
- What each bitwise operator does
- How to manage several flags in one integer
- The idioms for testing, adding, removing, and toggling a flag
- That JavaScript's bit operations run on 32-bit signed integers
Being able to read and write binary helps. This chapter touches on it briefly too.
Integers are sequences of bits
Computers hold integers as binary. On 8 bits the correspondence looks like this.
| Decimal | Binary (8 bits) |
|---|---|
| 1 | 00000001 |
| 2 | 00000010 |
| 4 | 00000100 |
| 5 | 00000101 |
| 8 | 00001000 |
The rightmost digit is the 1s place, the next is the 2s, then 4s — doubling as you move left. By convention positions are counted from the right as "bit 0", "bit 1". 5 is 00000101, so bits 0 and 2 are set.
Operators
& — AND
A bit is 1 only if both are 1.
00000101 (5)
& 00000011 (3)
------------
00000001 (1)
Used to test whether a specific bit is set.
| — OR
A bit is 1 if either is 1. Used to set a bit.
00000101 (5)
| 00000010 (2)
------------
00000111 (7)
^ — XOR
A bit is 1 when exactly one is 1. Used to toggle a bit. XORing twice with the same value returns the original.
00000101 (5)
^ 00000110 (6)
------------
00000011 (3)
~ — NOT (bitwise negation)
Inverts every bit. Used to build a mask for clearing bits.
<<, >> and >>> — shift
Shift the bit sequence left or right.
1 << 0 → 00000001 (1)
1 << 1 → 00000010 (2)
1 << 2 → 00000100 (4)
1 << 3 → 00001000 (8)
1 << n is "the value with only bit n set". This form is the basis of bit flags. Left shift by 1 doubles; right shift by 1 halves (rounded down). Both hold only within the 32-bit range and break outside it — 1 << 31 coming out negative is one example, covered in "JavaScript quirks".
>>> does not propagate the sign. Where >> fills with the sign bit, >>> fills with 0 from the left, so the result is always non-negative. x >>> 0 shifts by zero and performs only that conversion, which is how you read a 32-bit value back as non-negative.
Bit flags
A technique to hold multiple booleans in a single integer. Assign each boolean a distinct bit.
const Permission = {
Read: 1 << 0, // 1 = 00000001
Write: 1 << 1, // 2 = 00000010
Delete: 1 << 2, // 4 = 00000100
Admin: 1 << 3, // 8 = 00001000
} as const;
Values being powers of two matters. Because each uses a different digit, combining them with | doesn't mix information. Adding the same flag twice carries into another digit, so combine with |, not +.
Testing
/** Is flagBit set in value? */
function hasFlag(value: number, flagBit: number): boolean {
return (value & flagBit) !== 0;
}
AND leaves only the target bit; non-zero means it's set.
Try value = 5 (00000101).
| Call | Computation | Result |
|---|---|---|
hasFlag(5, Permission.Read) | 00000101 & 00000001 = 00000001 | true |
hasFlag(5, Permission.Write) | 00000101 & 00000010 = 00000000 | false |
hasFlag(5, Permission.Delete) | 00000101 & 00000100 = 00000100 | true |
5 represents "read and delete allowed, write forbidden".
You'll also see it written by taking a bit position. The substance is the same.
/** Is bit `position` set in value? */
function isBitSet(value: number, position: number): boolean {
return (value & (1 << position)) !== 0;
}
Add, remove, toggle
| Operation | Form | Mechanism |
|---|---|---|
| Set | value | flagBit | OR preserves 1 |
| Clear | value & ~flagBit | AND with a mask that's 0 only at the target |
| Toggle | value ^ flagBit | XOR is 1 when exactly one is 1 |
| Combine several | flagA | flagB | Different digits, so no mixing |
let perms = Permission.Read | Permission.Write; // 3 = 00000011
perms = perms | Permission.Delete; // 7 = 00000111 (grant delete)
perms = perms & ~Permission.Write; // 5 = 00000101 (revoke write)
perms = perms ^ Permission.Admin; // 13 = 00001101 (toggle admin)
hasFlag(perms, Permission.Write); // false
The idiom for "clear" uses ~. ~Permission.Write is a mask with only the Write bit zero and everything else one, so ANDing removes only the target.
Extracting a slice with a mask
To extract a run of contiguous bits, build a mask of that width.
// Extract the lower 4 bits
const lower4 = value & 0b1111; // 0b1111 is 15
// Extract bits 4 through 7 and shift them right
const upper4 = (value >> 4) & 0b1111;
(1 << width) - 1 builds "a mask of width width". 1 << 4 is 00010000; subtracting 1 gives 00001111.
What it's for
| Use | Example |
|---|---|
| Permission / option sets | File permissions, feature flags |
| Protocol headers | TCP flags, IP header fields |
| Compactly held state sets | Boolean attributes across many records |
| Fast set operations | Union = OR, intersection = AND — one instruction each |
Compared to holding booleans as separate properties, it's lighter to compare and to pass around. Testing multiple conditions at once takes a single AND.
The trade-off is readability drops. perms & 6 gives no clue as to intent. Constants with names, or wrapping tests in named functions, are the usual mitigations. For a few booleans with no performance need, the Set from Complexity, or a plain object, reads better. If you fold the flags into a database column, factor in that a plain column index no longer applies to per-flag filtering (Indexes).
JavaScript quirks
JavaScript and TypeScript bit operations behave differently from many other languages.
Operands are converted to 32-bit signed integers (the unsigned right shift >>> excepted). MDN describes it as "converts both operands to numbers, then to 32-bit integers, and performs the bit operation", with a 32-bit integer result. The representation is two's complement, so the range is -2147483648 to 2147483647. The unsigned right shift >>> doesn't propagate the sign — it shifts in 0 from the left — so its result lands in 0 to 4294967295.
Two consequences follow.
// 1. Bits beyond 32 are discarded
const big = 2 ** 33; // 8589934592
console.log(big | 0); // 0 ← upper bits lost
// 2. Setting bit 31 gives a negative
console.log(1 << 31); // -2147483648
console.log(1 << 32); // 1 ← shift amount wraps at 32
Setting bit 31 makes the value negative. Testing with & still works, so use x >>> 0 when you want a non-negative value for display or storage. If you need 33 or more flags, use BigInt or split across multiple numbers. BigInt-to-BigInt bit ops don't truncate to 32 bits.
const flag40 = 1n << 40n; // With BigInt, no overflow
In languages with fixed-width types like uint8 or uint32, out-of-range handling (truncation, overflow) is language-defined. When reading bit-op code in another language, check the type's width and signedness first.
Common misconceptions
"1 << 3 is 3." — It's 8. 1 << n is "only bit n set", which equals 2 to the n.
"Flag values can be sequential." — They must be powers of two (1, 2, 4, 8), or the digits overlap. 3 is 00000011, occupying both bit 0 and bit 1.
"JavaScript bit operations run on 64 bits." — They're converted to 32-bit signed. Numbers themselves are 64-bit floats, but bit operations truncate first.
"Bit flags are always efficient." — In exchange for compactness and fast testing, readability suffers. A few booleans are better held on an object.
Exercises
Q1. What does the following code print?
function isBitSet(value: number, position: number): boolean {
return (value & (1 << position)) !== 0;
}
const value = 10;
console.log(isBitSet(value, 0));
console.log(isBitSet(value, 1));
console.log(isBitSet(value, 3));
Answer and explanation
Answer: false / true / true from top to bottom
10 in binary is 00001010. Bits 1 and 3 are set.
| Call | 1 << position | AND result | |
|---|---|---|---|
isBitSet(10, 0) | 00000001 | 00000000 = 0 | false |
isBitSet(10, 1) | 00000010 | 00000010 = 2 | true |
isBitSet(10, 3) | 00001000 | 00001000 = 8 | true |
10 = 8 + 2, so bit 3 (the 8s place) and bit 1 (the 2s place) are set. That decomposition works in your head.
Q2. Revoke only "write" from a permission set. Which is correct?
const Write = 1 << 1; // 2
// A
perms = perms - Write;
// B
perms = perms & ~Write;
// C
perms = perms ^ Write;
// D
perms = perms | ~Write;
Answer and explanation
Answer: B
- A (
- Write) — breaks when the user never had write. Fromperms = 5(00000101), subtracting 2 gives3(00000011), which sets bit 1 that wasn't set - B (
& ~Write) —~Writeis a mask with only bit 1 zero, everything else one. AND clears the target while preserving the rest. Correct whether it was set or not - C (
^ Write) — toggles. If it wasn't set, it sets it. That's a toggle, not a revoke - D (
| ~Write) — OR sets everything except bit 1. That's close to granting everything
"Clear with & ~" is the idiom. Subtracting depends on state and breaks.
Q3. You tried to manage 40 flags in one number and it didn't work. Why?
Answer and explanation
Answer: JavaScript's bit operations are performed as 32-bit signed integers.
1 << 40 doesn't give the expected value. The shift amount wraps at 32, so 1 << 40 equals 1 << 8, which is 256. Bit 32 and beyond aren't representable.
And the moment you use bit 31, the result becomes negative (1 << 31 is -2147483648).
Two countermeasures.
- Use
BigInt—1n << 40ndoesn't truncate. Note that mixing with regular numbers throwsTypeError - Split across multiple numbers — group into sets of 32 in an array
Beyond mechanics, revisiting the design of 40 flags is also worthwhile. At that scale, Set or an object reads better and is easier to maintain.
Summary
&tests,|sets,^toggles,~builds masks1 << nis "only bit n set" — the base form of bit flags- Flag values are powers of two. Sequential values overlap digits
- Set with
| flag, clear with& ~flag, toggle with^ flag. Clearing by subtraction depends on state and breaks - JavaScript bit ops convert to 32-bit signed. Setting bit 31 makes the value negative, and bit 32 and beyond can't be represented
- Compactness and fast testing come at the cost of readability. Ordinary objects fit better for a few booleans
- MDN — Bitwise AND — Rules for the conversion to 32-bit integer
- JavaScript Guide — Operators and number handling
What to read next
- Design patterns — Talking about design in a shared vocabulary