JAVASCRIPT / VALUES, TYPES, AND COERCION
Numbers, strings, and the rest of the type list
Name all seven primitive types, say what a number and a string actually store, and predict the limits those representations impose.
What you will learn
- List the seven primitive types and recognise arrays, functions and dates as objects
- Explain why 2 ** 53 + 1 === 2 ** 53 and switch to bigint literals when it matters
- Predict .length for escaped and astral characters instead of guessing
- Choose between 0xff, 1_000_000, 10n and backtick literals with a reason
Understanding Numbers, strings, and the rest of the type list
JavaScript's type list is closed and short: number, string, boolean, undefined, null, symbol and bigint are the seven primitive types, and everything else is an object. Arrays, functions, dates, regular expressions and Map instances are not extra types, they are objects with different built-in behaviour. The type is a property of the value, not of the variable that holds it, which is why you never write int or String in a declaration. Once you know the list, every value you ever inspect is one of eight kinds.
The number type is a single 64-bit IEEE-754 double, so there is no separate integer type: 5 and 5.0 are the same value, and 7 / 2 gives 3.5 rather than 3. A double carries 53 bits of integer precision, so integers stay exact only up to 9007199254740991, exposed as Number.MAX_SAFE_INTEGER; past that, neighbouring integers collapse onto the same value. Infinity, -Infinity and -0 are also members of the number type, not error markers. bigint was added later for exact integers of any size, and it deliberately refuses to mix with number in arithmetic so that precision can never be lost by accident.
A string is an immutable sequence of UTF-16 code units. Single quotes, double quotes and backticks all produce that same type; only backticks interpolate expressions and span lines. Because strings are immutable, methods like toUpperCase or trim cannot change anything, they return a new string, and .length reports code units rather than characters a reader would count, so a character outside the basic plane counts as two. The three remaining primitives are simpler: boolean has exactly two values, undefined and null each have one, and symbol produces values that are guaranteed unique so they can be used as property keys that never collide.
placeholder
// number: one type for integers and fractions
console.log(7 / 2);
console.log(Number.MAX_SAFE_INTEGER);
// bigint: a separate type for exact integers of any size
console.log(9007199254740993n + 1n);
// string: an immutable sequence of UTF-16 code units
const greeting = "hola";
console.log(greeting.toUpperCase());
console.log(greeting);
console.log("\u{1F642}".length);
// the remaining primitives
console.log(true, undefined, null);
console.log(Symbol("id"));
// anything that is not a primitive is an object
console.log([1, 2] instanceof Object);JavaScript has exactly seven primitive types plus objects, and the type belongs to each value, not to the variable holding it.
Worked examples
Where the number type stops being exact
Shows the precision boundary of doubles and the number values that beginners mistake for errors.
console.log(0.1 + 0.2);
console.log(2 ** 53 === 2 ** 53 + 1);
console.log(0b1010, 0o17, 0xff, 1_000_000);
console.log(1 / 0, -1 / 0);
console.log(Object.is(-0, 0));Example explained
Line 10.1 + 0.2 lands just above 0.3 because tenths have no exact binary fraction, the same way base ten cannot write 1/3.
Line 22 ** 53 + 1 would need 54 bits of integer precision, so it rounds back down to 2 ** 53 and the comparison is true.
Line 30b, 0o, 0x and the _ separator are only ways of writing the same number type; nothing records which form you typed.
Line 4Infinity, -Infinity and -0 are ordinary number values, which is why Object.is can distinguish -0 from 0.
Strings are code units, and they never change
Demonstrates escape sequences, the difference between .length and code points, and string immutability.
const name = "Zo\u00EB";
console.log(name, name.length);
const smile = "\u{1F642}";
console.log(smile.length, [...smile].length);
console.log(name.slice(0, 2) + "e");
console.log(name);Example explained
Line 1\u00EB is an escape for one code unit, so name holds three of them and .length is 3.
Line 2The smiling face is a single code point stored as a surrogate pair, so .length reports 2 while spreading, which iterates code points, reports 1.
Line 3slice plus + builds a brand-new string; nothing is written back into name.
Line 4Logging name afterwards shows the original untouched, which is what immutable means in practice.
bigint and symbol, the two newer primitives
Shows bigint's exact integer arithmetic, its refusal to mix with number, and symbol uniqueness.
const big = 2n ** 64n;
console.log(big);
console.log(big / 3n);
try {
console.log(big + 1);
} catch (err) {
console.log(err.constructor.name);
}
console.log(Symbol("key") === Symbol("key"));Example explained
Line 1A bigint literal ends in n, and the console keeps that suffix so 1n is never confused with 1.
Line 2Dividing bigints truncates toward zero because the type stores only integers, so no fractional part survives.
Line 3big + 1 throws TypeError on purpose: allowing the mix would silently drop precision from the bigint side.
Line 4Two symbols built from the same description are still different values, which is what makes them collision-proof keys.
Important notes
word[0] = "b" does nothing in sloppy mode but throws TypeError inside a module or a strict-mode function, so the identical line behaves differently depending on where it runs.
Arithmetic never mixes bigint with number (1n + 1 throws), yet relational comparison across the two types is allowed: 1n < 2 is true.
Common mistakes
Expecting 10 / 4 to be 2 because both operands look like integers; the result is 2.5, and using it as an index like arr[2.5] quietly reads undefined.
Keeping API identifiers above 9007199254740991 as numbers; the value is rounded the moment it is parsed, so 9007199254740993 becomes 9007199254740992 and every lookup with it misses.
Calling greeting.toUpperCase() without assigning the result, then wondering why greeting is unchanged; the method returned a new string that was thrown away.
Try it yourself
Change, predict, then run
In the browser console, declare one variable for each of the seven primitive types using its own literal form, then log Number.MAX_SAFE_INTEGER + 2 next to BigInt(Number.MAX_SAFE_INTEGER) + 2n and add a comment explaining why the two results disagree.
Open the JavaScript workspaceCheck your understanding
An API returns an order id of 9007199254740993. You parse it into a JavaScript number and log it, and 9007199254740992 appears. What happened?
- JavaScript numbers are 32-bit integers, so the id overflowed
- console.log shortens long numbers when displaying them, but the stored value is still exact
- Numbers are IEEE-754 doubles with 53 bits of integer precision, so that odd integer is not representable and was rounded to the nearest value that is
- The id was stored as a bigint, and bigint drops the last digit of very large values
Show answer
A double keeps 53 bits of significand, so above 2 ** 53 only even integers are representable and 9007199254740993 rounds to 9007199254740992. The display option is tempting because the change is first noticed in output, but the loss happens at parse time: comparing the variable to 9007199254740992 is true, and no formatting is involved. Parsing into a bigint instead keeps the digit, since bigint has no size limit and truncates nothing.