← Fundamentals
Guided lesson 6 questions · 6 languages

Masks, shifts, and bitwise operations

AND, OR, XOR, and NOT on single bits, flags in one integer, shifts, and how these operators group.

A bitwise operator works on each bit position separately. Give each yes-or-no fact its own bit, a power of two, and one integer holds several flags: OR sets a flag, AND with its NOT clears it, AND tests it, and XOR flips it. Shifts move bits left or right. Put parentheses around bitwise operations, because each language groups them differently.

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.

Go to the final check· Go to the practice tickets

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.

main.ts
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
0110

02 / 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”.

main.ts
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 true

03 / 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.

main.ts
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
-2147483648

04 / 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.

main.ts
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

true

05 / & 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.

main.ts
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.

main.ts
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

  1. BITOP-101 · The take-off warning shows with every switch off → Free
  2. BITOP-102 · Silencing a silent step changes other steps → Sign in · Pro
  3. BITOP-103 · Left turns come out as sharp right turns → Sign in · Pro

Keep exploring

Conditions and truthiness →

Primary references