Before you begin
Why this matters
Flags in one number appear in file permissions, colour values, network data, and settings. TypeScript adds its own surprises: every bitwise operator works on 32 bits, so a large number can change and a flag in bit 31 is negative.
What this helps you reason about
- Predict &, |, ^, and ~ on two numbers written in binary.
- Set, clear, test, and toggle a flag, and spot a clear that clears the wrong bits.
- Predict >> and >>> on a negative number, and know why (x & FLAG) === 0 needs its parentheses.
An aquarium controller keeps four on-or-off devices in one small number: a light, a pump, a heater, and a feeder. This lesson shows how TypeScript works on single bits, how one number holds several flags, what shifts do, and how these operators group with === and &&.
Already know this? The short version
- & | ^ ~ work bit by bit on 32-bit signed integers: the number is converted first, keeping its low 32 bits, so 2 ** 32 + 5 | 0 is 5 and ~12 is -13.
- Flags are powers of two (1 << n): set x | F, clear x & ~F, test (x & F) !== 0 (not > 0: bit 31 is negative), toggle x ^ F.
- << and >> use the low 5 bits of the count (1 << 32 is 1); >> is arithmetic, >>> is logical and gives an unsigned result (-1 >>> 0 is 4294967295).
- === comes before &, so x & F === 0 is a compile error; & on two booleans is refused too. Use bigint for more than 32 bits.
Words in this lesson (7)
- bit
- A single binary digit: 0 or 1.
- bitwise operator
- An operator that works on each bit position of its operands separately, such as & (AND), | (OR), ^ (XOR), and ~ (NOT).
- two’s complement
- The usual way to store a signed integer: the top bit counts as minus its place value. In 8 bits, 10000000 is -128, 11111111 is -1, and 01111111 is 127.
- flag
- One bit that records a yes-or-no fact, such as whether a pump is running. Several flags can share one integer, one bit each.
- mask
- An integer whose 1 bits pick out the bits an operation looks at or changes. The mask 0100 picks out bit 2.
- shift
- Moving every bit of an integer a number of places to the left or to the right. Bits that move past either end are dropped.
- logical shift and arithmetic shift
- Two ways to shift right. A logical shift fills the empty places on the left with 0. An arithmetic shift copies the sign bit, so a negative number stays negative.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Bit operators
What do &, |, ^, and ~ do to each bit?
Bits, bytes, and binary showed an integer as a row of bits. Bits are numbered from the right, starting at 0, so bit n is worth 2 to the power n. A bitwise operator works on that row one position at a time: it pairs bit 0 of one operand with bit 0 of the other, bit 1 with bit 1, and so on. No position affects another, and nothing is carried.
& (AND) gives 1 where both bits are 1. | (OR) gives 1 where either bit is 1. ^ (XOR, “exclusive or”) gives 1 where the two bits differ. ~ (NOT) has one operand and flips every bit: 0 becomes 1 and 1 becomes 0. Before any of them works, TypeScript converts the number to a 32-bit signed integer. So ~ flips all 32 bits, and ~12 is -13 in two’s complement.
const morning = 0b1100; // 12
const evening = 0b1010; // 10
console.log(morning & evening); // 8: 1000
console.log(morning | evening); // 14: 1110
console.log(morning ^ evening); // 6: 0110
console.log(~morning); // -13 AND gives 1 where both bits are 1, OR where either is 1, XOR where they differ, and NOT flips every bit.
Precisely: the 32-bit conversion
The ECMAScript operators apply ToInt32 to each operand: the number is rounded toward zero, and only its low 32 bits are kept. So 2 ** 32 + 5 | 0 is 5, and 3.7 | 0 is 3. Above 2^31 - 1 a number does not survive a bitwise operation; bigint operands, such as 5n & 3n, have no such limit.
Experiment
Combine two lamp patterns bit by bit
Each operator looks at one position at a time. 1100 and 1010 are both 1 only in the left position, so & gives 1000. At least one is 1 in three positions, so | gives 1110. They differ in the middle two positions, so ^ gives 0110. A value combined with itself by ^ gives 0, because no position differs. toString(2).padStart(4, "0") writes the result in binary with 4 digits, as [Bits, bytes, and binary](bits-bytes-binary) showed.
Why it matters & keeps what both have, | keeps what either has, and ^ keeps what differs.
Predict the three results. Then change evening = 0b1010 to evening = 0b1100 so that both patterns are the same, and predict again.
const morning = 0b1100;
const evening = 0b1010;
console.log((morning & evening).toString(2).padStart(4, "0"));
console.log((morning | evening).toString(2).padStart(4, "0"));
console.log((morning ^ evening).toString(2).padStart(4, "0"));
What to look for
Step past lines 1 and 2: morning shows 12 and evening 10, the values of 1100 and 1010. The debugger may stop on one line several times; that is normal for it. The run prints 1000, 1110, then 0110. After the edit, it prints 1100, 1100, then 0000.
Expected output
1000
1110
011002 / Flags
How can one integer carry several flags?
A flag is one bit that records a yes-or-no fact. An aquarium controller can keep four flags in one integer: the light is bit 0, the pump bit 1, the heater bit 2, and the feeder bit 3. Each flag is a power of two, 1, 2, 4, and 8, so each one has exactly one 1 bit. An integer whose 1 bits pick out the bits to work on is called a mask; a single flag is the simplest mask.
Four operations cover almost every use. running | PUMP sets the pump flag and leaves the other bits as they are. running & ~PUMP clears it: ~PUMP has a 1 in every bit except the pump bit, so & keeps every other bit and clears that one. (running & PUMP) !== 0 tests it: & keeps only the pump bit, and the result is 0 exactly when the pump is off. running ^ PUMP toggles it: on becomes off, and off becomes on.
Setting a flag that is on, or clearing one that is off, changes nothing. Toggling always changes the bit, so use ^ only when you mean “switch to the other state”.
const LIGHT = 1; // bit 0
const PUMP = 2; // bit 1
const HEATER = 4; // bit 2
let running = LIGHT | HEATER; // 0101
running = running | PUMP; // 0111
running = running & ~HEATER; // 0011
console.log((running & PUMP) !== 0); // true
console.log((running & HEATER) !== 0); // false Set a flag with OR, clear it with AND and the NOT of the flag, test it with AND and a comparison with 0, and toggle it with XOR.
Coming from Python: Python’s integers have no fixed width, so 1 << 40 is 1099511627776; TypeScript’s bitwise operators work on 32 bits, and 1 << 40 is 256.
Precisely: test against 0, not with > 0
A flag in bit 31, 1 << 31, is -2147483648, so (x & FLAG) > 0 is false even when the flag is on. !== 0 works for every bit. To test several flags at once, (x & (A | B)) !== 0 is true when either is on, and (x & (A | B)) === (A | B) when both are on.
Experiment
Set, clear, and test aquarium flags
Setting the light gives 0001, setting the pump gives 0011. The NOT of the light flag is 1 everywhere except bit 0, so & with it keeps the pump and clears the light: 0010. Without the NOT, & keeps only the bits that are 1 in the light flag, so the pump is cleared instead.
Why it matters Clear a flag with AND and the NOT of the flag. AND with the flag itself keeps that flag and clears every other one.
Predict running after each line, and the printed line. Then change & ~LIGHT to & LIGHT and predict again.
const LIGHT = 1;
const PUMP = 2;
let running = 0;
running = running | LIGHT;
running = running | PUMP;
running = running & ~LIGHT;
console.log(running, (running & PUMP) !== 0);
What to look for
Step through and watch running: 0, then 1, then 3, then 2. The run prints 2 true. After the edit, running goes 0, 1, 3, then 1, and the run prints 1 false.
Expected output
2 true03 / Shifts
What do << and >> do to the bits?
x << n moves every bit n places to the left and fills the empty places on the right with 0. Each place doubles the value, so 1 shifted left by 3 is 8, as long as no 1 bit moves past the top of the type. That is how a flag for bit n is usually written. x >> n moves the bits n places to the right, and the bits that move past the right end are dropped. For a number that is not negative, that divides by 2 to the power n and rounds down: 13 shifted right by 2 is 3.
A right shift must also fill the empty places on the left. A logical shift fills them with 0. An arithmetic shift copies the top bit, which is the sign bit in two’s complement, so a negative number stays negative: -16 shifted right by 2 is -4.
TypeScript has both right shifts. >> is arithmetic, so -16 >> 2 is -4. >>> is logical, and it reads the result as an unsigned 32-bit number: -16 >>> 2 is 1073741820, and -1 >>> 0 is 4294967295. Only the low 5 bits of the count are used, so 1 << 32 is 1, not 2 to the power 32. 1 << 31 puts a 1 into the sign bit, so it is negative.
console.log(1 << 3); // 8
console.log(13 >> 2); // 3
console.log(-16 >> 2); // -4
console.log(-16 >>> 2); // 1073741820
console.log(1 << 31); // -2147483648
console.log(1 << 32); // 1 A left shift by n multiplies by 2 to the power n until bits fall off the top; a right shift divides, and the type or the operator decides what fills the left.
Experiment
Shift a negative number and a high bit
-16 as a 32-bit integer is 11111111111111111111111111110000. >> copies the sign bit, so -16 >> 2 is -4. >>> fills with 0 and reads the result as unsigned: 1073741820. 1 << 31 puts a 1 into bit 31, the sign bit of a 32-bit integer, so the result is negative. The shift count uses its low 5 bits only, so 1 << 32 is 1 << 0.
Why it matters TypeScript’s bitwise operators work on 32 bits. A flag in bit 31 is negative, and there is no bit 32.
Predict both printed lines. Then change 1 << 31 to 1 << 32 and predict the second line again.
const level = -16;
console.log(level >> 2, level >>> 2);
const highest = 1 << 31;
console.log(highest);
What to look for
Step past line 1: level shows -16. The run prints -4 1073741820, then highest shows -2147483648, and the run prints it. After the edit, highest shows 1, and the second line is 1.
Expected output
-4 1073741820
-214748364804 / Grouping
How do bitwise operators group with == and +?
An expression with several operators is grouped by operator precedence, as Evaluation order showed for * and +. The bitwise operators have their own places in that order, and the places differ between languages. In this language, & comes after the comparisons. So running & PUMP === 0 means running & (PUMP === 0). Among the bitwise operators, & comes before ^, and ^ before |, as * comes before +: a & b | c means (a & b) | c. The shifts come after + and -: 1 << n - 1 means 1 << (n - 1). Put parentheses around every bitwise operation inside a larger expression.
TypeScript catches the first mistake. running & PUMP === 0 means running & a boolean, and the compiler reports The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. The other two groupings compile and give a different number.
const LIGHT = 1;
const HEATER = 4;
const running = 3;
console.log((running & HEATER) === 0); // true
console.log(running & LIGHT | HEATER); // 5: (3 & 1) | 4
console.log(running & (LIGHT | HEATER)); // 1 Put parentheses around a bitwise operation inside a comparison or a sum; the grouping differs between languages.
Experiment
Remove the parentheses around a flag test
Without the parentheses, === is applied first, so the expression is running & (PUMP === 0): a number & a boolean. TypeScript refuses it.
Why it matters In TypeScript, a missing pair of parentheses around & is a compile error, not a wrong answer. Write (x & flag) !== 0.
Predict pumpOff. Then change (running & PUMP) === 0 to running & PUMP === 0 and predict whether the program runs.
let running = 1;
const PUMP = 2;
const pumpOff = (running & PUMP) === 0;
console.log(pumpOff);
What to look for
Step past line 3: pumpOff shows true, and the run prints true. After the edit, nothing runs; the compiler reports Line 3: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. A second line adds that the comparison of 2 with 0 looks unintentional.
Expected output
true05 / & and &&
How is & different from &&?
&& is logical AND. It works on two whole true-or-false values, not on bits, and it computes its right side only when the left side is true. Short-circuit evaluation, a later lesson, shows what that skipping means. A bitwise AND always computes both sides and combines every bit position. So 2 & 1 is 0, because 10 and 01 share no bit, while 2 && 1 treats both numbers as true.
TypeScript refuses & on two booleans: with a and b booleans, a & b gives The '&' operator is not allowed for boolean types. Consider using '&&' instead. A flag test inside a condition joins with &&: (running & PUMP) !== 0 && (running & LIGHT) === 0.
const running = 2;
const PUMP = 2;
const LIGHT = 1;
const pumpOnly = (running & PUMP) !== 0 && (running & LIGHT) === 0;
console.log(pumpOnly); // true
console.log(2 & 1); // 0 Use & to combine bits and && to combine true-or-false results.
Coming from Java: Java allows & on two booleans and computes both sides; TypeScript refuses it.
06 / Try it
Can you explain a fresh example?
Follow the flags one operation at a time, in binary. Conditions and truthiness comes next: it asks what may stand in a condition, including a number such as a flag test.
let running = 0b0101;
running = running | 0b0010;
running = running & ~0b0100;
running = running ^ 0b1000;
console.log(running, running >> 1, (running & 0b0100) !== 0);
What is printed?
Reveal the reasoning
11 5 false. 0101 | 0010 is 0111. ~0100 has a 0 only in bit 2, so & clears that bit: 0011. ^ 1000 flips bit 3 from 0 to 1: 1011, which is 11. 1011 shifted right by 1 is 101, which is 5. Bit 2 is 0, so the test is false.
Practice tickets
- BITOP-101 · The take-off warning shows with every switch off → Free
- BITOP-102 · Silencing a silent step changes other steps → Sign in · Pro
- BITOP-103 · Left turns come out as sharp right turns → Sign in · Pro