Before you begin
Why this matters
Binary and hexadecimal appear in colours such as #ff8800, in file contents, and in debugger output. When you can read them by hand, you can check a value instead of guessing. You will also recognize a common bug: treating the character "7" as the number 7.
What this helps you reason about
- Say the decimal value of a binary or hexadecimal number.
- Print a number in binary or hexadecimal, padded to a fixed width.
- Turn a string of digits into a number, and spot parseInt accepting text that is not a number in its base.
The number eleven can be written as 11, as 1011, or as b, depending on the base. This lesson shows how a row of bits stands for a number, how to write and print numbers in binary and hexadecimal, and how to turn a string of digits into a number yourself.
Already know this? The short version
- In base 2 each place is worth twice the place to its right, in base 16 sixteen times; 0b1011, 0xb, and 11 are one number, and one hexadecimal digit is four bits.
- A byte is 8 bits: 256 patterns, 0 to 255, or 00 to ff. TypeScript has one number type; a Uint8Array stores a list of bytes.
- n.toString(2) and n.toString(16) return digits without a prefix; padStart(8, "0") pads and never cuts. 010 is a compile error; octal is 0o10.
- By hand: value = value * base + digit, from the left; the character "7" is not the number 7. parseInt(text, 2) stops silently at the first character that is not a digit of the base, so parseInt("1021", 2) is 2; Number("0b" + text) gives NaN instead.
Words in this lesson (8)
- bit
- A single binary digit: 0 or 1.
- place value
- What a digit is worth because of where it stands. In decimal, each place is worth ten times the place to its right; in binary, two times; in hexadecimal, sixteen times.
- base
- The number of different digits a way of writing numbers uses: 10 for decimal, 2 for binary, 16 for hexadecimal. Each place is worth the base times the place to its right.
- binary
- Writing numbers in base 2, with the digits 0 and 1. Binary 1011 is 8 + 2 + 1 = 11.
- hexadecimal
- Writing numbers in base 16, with the digits 0 to 9 and a to f, where a is 10 and f is 15. One hexadecimal digit stands for exactly four bits.
- literal
- A value written directly in the code, such as 11, 0b1011, or 0xb.
- byte
- A unit of 8 bits, the smallest piece of memory a program usually reads or writes. One byte can hold a number from 0 to 255.
- character code
- The number a program stores for a character. The digit characters 0 to 9 have the codes 48 to 57, so the character 7 is stored as 55, not 7.
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 / Place value
How can a row of bits stand for a number?
A bit is a single binary digit: 0 or 1. A row of bits can stand for a number, in the same way that a row of decimal digits does.
In the decimal number 345, the 3 is worth 300 because of where it stands. Each place is worth ten times the place to its right: ones, tens, hundreds. This is called place value. The number of different digits is the base, so decimal is base 10. Binary is base 2: it has the digits 0 and 1, and each place is worth two times the place to its right: 1, 2, 4, 8, and so on. So binary 1011 is 1 × 8 + 0 × 4 + 1 × 2 + 1 × 1 = 11.
You can also read the digits from left to right and keep a value so far. For each digit, multiply the value so far by 2, then add the digit. The loop below does that. const bits = [1, 0, 1, 1] is an array, a list of values in order, and for (const bit of bits) runs the block once for each element, from first to last.
const bits = [1, 0, 1, 1];
let value = 0;
for (const bit of bits) {
value = value * 2 + bit;
}
console.log(value); // 11 Each digit is worth the digit times its place. Reading from the left, multiply the value so far by the base and add the next digit.
Precisely: why multiplying works
After the last digit, the first digit has been multiplied by the base once for every digit after it, which is exactly its place value. With 10 instead of 2, the same steps read decimal digits: 3, then 34, then 345.
Experiment
Build a number one bit at a time
Multiplying by 2 moves every bit already read one place to the left, which doubles what it is worth. Adding the next bit fills the new ones place. After four bits, the first 1 has been doubled three times, so it is worth 8.
Why it matters The same digits mean different numbers in different bases. The base is the number you multiply by.
Predict the four printed values. Then change value * 2 to value * 10 and predict again.
const bits = [1, 0, 1, 1];
let value = 0;
for (const bit of bits) {
value = value * 2 + bit;
console.log(value);
}
What to look for
Step through the loop and watch bit and value. value goes 0, 1, 2, 5, 11, and the run prints 1, 2, 5, 11, one per line. After the edit, value goes 1, 10, 101, 1011: the same digits read as a decimal number. Step sometimes stops on the same line twice; that is normal for this debugger.
Expected output
1
2
5
1102 / Literals
How do you write a number in binary or hexadecimal?
A value written directly in the code is a literal. 0b1011 is a binary literal and 0xb is a hexadecimal literal. Both are the number 11, and so is the decimal literal 11. === compares numbers by value, so 0b1011 === 0xb is true. The base changes how a number is written, not the number.
Hexadecimal is base 16. It needs sixteen digits, so after 0 to 9 it uses the letters a to f: a is 10, b is 11, and f is 15. Capital letters A to F mean the same. Each place is worth sixteen times the place to its right, so hexadecimal 2a is 2 × 16 + 10 = 42.
Programmers use hexadecimal because one hexadecimal digit stands for exactly four bits: four bits have 16 patterns. Hexadecimal b is binary 1011, and hexadecimal 2a is binary 0010 1010: the 2 gives the left four bits and the a gives the right four.
const decimal = 11;
const binary = 0b1011;
const hex = 0xb;
console.log(decimal === binary, binary === hex); // true true
console.log(0x2a); // 42 A binary or hexadecimal literal is another way to write the same number. One hexadecimal digit is four bits.
Precisely: octal and separators
0o17 is an octal literal, base 8, so it is 15. The old form with a leading zero is refused: const n = 010; gives the error Octal literals are not allowed. Use the syntax '0o10'. An underscore can group digits for reading, as in 0b1011_0110 (ES2021 and later); it does not change the value.
03 / Bytes
What is a byte, and what can it hold?
A byte is a group of 8 bits. Memory and files are measured in bytes.
Each bit has two possible values, so 8 bits have 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 different patterns. Read as a number, they run from 00000000, which is 0, to 11111111, which is 255. In hexadecimal, a byte is always two digits, from 00 to ff. That is why tools often show bytes as pairs of hexadecimal digits, such as 48 69 ff.
TypeScript has no type for one byte: every number has the type number. A Uint8Array holds a list of bytes, each from 0 to 255. What happens to a value that does not fit, and how negative numbers are stored, is the subject of a later lesson, Integers, signs, and overflow.
console.log(0b11111111); // 255
console.log(0xff); // 255
const bytes = new Uint8Array([0x48, 0x69, 0xff]);
console.log(bytes[2]); // 255 A byte is 8 bits: 256 patterns, read as 0 to 255, or two hexadecimal digits.
Precisely: how a number is stored
A TypeScript number is stored in 64 bits as a floating-point value, not as a plain binary integer. Every whole number up to 2 to the power 53 is stored exactly, so the bytes and binary values in this lesson behave as written. To read a number stored in several bytes, a DataView has methods such as getUint16, which read the byte worth the most first (big-endian) unless you pass true for the opposite order (little-endian).
04 / Printing
How do you print a number in binary or hexadecimal?
console.log shows a number in decimal. n.toString(2) returns the binary digits of n as a string, and n.toString(16) returns the hexadecimal digits, with small letters. Neither adds 0b or 0x.
To show all 8 bits of a byte, pad the string with zeros on the left. padStart(8, "0") adds zeros until the string has 8 characters. It never removes characters, so a number above 255 still shows all of its bits.
const value = 42;
console.log(value.toString(2)); // 101010
console.log(value.toString(2).padStart(8, "0")); // 00101010
console.log(value.toString(16)); // 2a
console.log(value.toString(16).toUpperCase()); // 2A toString(2) and toString(16) give the digits without a prefix; padStart adds zeros and never cuts.
Coming from Python: n.toString(16) gives "2a", without the "0x" that Python’s hex(n) adds; there is no format(n, "08b"), so pad with padStart.
Experiment
Print one number in binary and hexadecimal
toString(2) and toString(16) give the digits with no prefix. padStart(8, "0") adds zeros until the string is 8 characters long. The width 8 is a minimum: zeros are added on the left until there are 8 digits, and nothing is ever removed. 300 is 256 + 32 + 8 + 4, so it needs a ninth place, worth 256.
Why it matters Padding to 8 digits shows every bit of a byte, but it does not limit a number to one byte. A number above 255 prints with more digits.
Predict the three printed lines. Then change 11 to 300 and predict again: how many binary digits does 300 need?
const value = 11;
console.log(value.toString(2));
console.log(value.toString(2).padStart(8, "0"));
console.log(value.toString(16));
What to look for
Step past each line and watch the output. The run prints 1011, then 00001011, then b. After the edit, it prints 100101100 twice, because padStart adds nothing to a string that already has 9 characters, then 12c.
Expected output
1011
00001011
b05 / Digits in text
How do you turn a string of digits into a number?
Text such as "101" is not a number. It is a string of three characters. A program stores each character as a number of its own, called its character code. The digit characters 0 to 9 have the codes 48 to 57, in order. So the character 1 is stored as 49, not as 1, and the character 7 as 55.
To get the value of a digit character, subtract the code of the character 0: 49 - 48 is 1. Then use the loop from the first section. In TypeScript, text[i] is the character at position i, as a string of length one, and text.charCodeAt(i) gives its code. Number(text[i]) gives the value of one digit character from "0" to "9". for (let i = 0; i < text.length; i++) counts i from 0 up to the last position; i++ adds 1 to i.
TypeScript refuses value * 2 + text[i]: + with a string joins text instead of adding. It reports Type 'string' is not assignable to type 'number'.
parseInt(text, 2) does the same work for you, with one trap. It stops at the first character that is not a digit of the base and returns what it has read so far, with no error. Number("0b" + text) reads the whole text as a binary literal, and gives NaN, the special number meaning “not a number”, when any character does not fit.
const text = "101";
let value = 0;
for (let i = 0; i < text.length; i++) {
value = value * 2 + Number(text[i]);
}
console.log(value); // 5
console.log(parseInt(text, 2)); // 5 Turn each digit character into its digit, then multiply the value so far by the base and add.
Coming from Python: int("1021", 2) raises an error; parseInt("1021", 2) returns 2 without one.
Precisely: what else parseInt accepts
parseInt also skips spaces at the start, accepts a + or - sign, and without a base reads "0x1f" as hexadecimal because of the prefix. Always pass the base. Number accepts the prefixes 0b, 0o, and 0x and spaces around the digits, and gives 0 for an empty string.
Experiment
Read a binary string with parseInt and Number
parseInt reads digits until it meets a character that is not a digit of the base, here the 2, and returns what it has read so far: binary 10, which is 2. Number reads the whole string "0b1021" as a binary literal, and when any character does not fit, the result is NaN.
Why it matters parseInt can turn wrong text into a believable number. When every character must be a digit, use Number("0b" + text) and check for NaN, or check the digits yourself.
Predict both printed numbers. Then change "1021" to "1011" and predict again.
const typed = "1021";
const loose = parseInt(typed, 2);
const strict = Number("0b" + typed);
console.log(loose, strict);
What to look for
Step past each line and watch loose and strict. loose shows 2 and strict shows NaN, “not a number”, and the run prints 2 NaN. Nothing reports an error. After the edit, both show 11, and the run prints 11 11.
Expected output
2 NaN06 / Try it
Can you explain a fresh example?
Work out what each literal is worth, then how each number is printed. The next lesson, Function calls, follows the values that functions receive and return.
const a = 0x1f;
const b = 0b1000;
console.log(a + b, (a + b).toString(16), "7" + 1);
What is printed?
Reveal the reasoning
39 27 71. 0x1f is 1 × 16 + 15 = 31, and 0b1000 is 8, so a + b is 39. In hexadecimal, 39 is 2 × 16 + 7, which prints as 27: the same digits as decimal twenty-seven, but a different number. "7" + 1 joins a string and a number into the string "71"; the character "7" is not the number 7.
Practice tickets
- BITS-101 · The pill counter shows 3 for a tray of 6 pills → Free
- BITS-102 · Heart rates saved with a letter come out wrong → Sign in · Pro
- BITS-103 · Impossible hearing aid settings are saved as other values → Sign in · Pro