Types & values
Eight types, one conversion table, and the equality check that never stops mattering.
Seven primitives + object
JavaScript has exactly eight types. Seven are primitives; everything else — arrays, functions, dates, regular expressions — is an object. Two things define a primitive: it's immutable, and it's copied by value.
| Primitive | Example | typeof |
|---|---|---|
| string | "hi" |
"string" |
| number | 42, 3.14, NaN |
"number" |
| bigint | 10n |
"bigint" |
| boolean | true, false |
"boolean" |
| undefined | undefined |
"undefined" |
| null | null |
"object" — a bug, see below |
| symbol | Symbol("id") |
"symbol" |
| — everything else — | arrays, functions, dates, {}… |
"object" or "function" |
Change the copy and watch what happens to the original. This one difference explains most "why did my array change?" bugs — and it's exactly what the "Copy without sharing" exercise below is testing.
let s = "hello";
s[0] = "H"; // silently ignored
console.log(s); // still "hello"
console.log(s.toUpperCase()); // "HELLO" — a NEW string
console.log(s); // still "hello"
s.custom = 1; // silently ignored too
console.log(s.custom); // undefined
So why does "abc".length work at all, if strings can't hold
properties? Autoboxing — the engine wraps the primitive in a
throwaway String object, reads the property, then discards
the wrapper. That's also why s.custom = 1 above does
nothing: you wrote to something that was already gone.
typeof — and its two lies
typeof returns one of eight strings, and it's the one
operator that can safely touch an undeclared name without throwing. It
tells the truth about primitives — but two of its answers are traps.
| Expression | typeof | Note |
|---|---|---|
typeof null |
"object" |
a 1995 bug kept forever for compatibility — check
value === null instead
|
typeof [] |
"object" | arrays are objects — use Array.isArray(v) |
typeof function(){} |
"function" | the one honest special case — functions are still objects underneath |
typeof undeclaredName |
"undefined" | no ReferenceError — safe to use as a feature check |
console.log(typeof null); // the trap
console.log(typeof []); // also a trap
console.log(Array.isArray([])); // the fix
console.log(typeof (() => {}));
console.log(typeof Symbol("x"));
console.log(typeof 10n);
null vs undefined
Both mean "no value" — the difference is who wrote it.
undefined is absence by default, handed to you by
JavaScript. null is absence on purpose, assigned by a
developer.
| Default parameter fires? | Survives JSON.stringify? |
|
|---|---|---|
undefined |
yes | no — the key is dropped |
null |
no — null is a real value, not "missing" | yes, kept |
function greet(name = "friend") { return "Hi " + name; }
console.log(greet(undefined)); // default fires
console.log(greet(null)); // default does NOT fire
const v1 = 0, v2 = null;
console.log(v1 ?? "fallback"); // 0 — a real value survives
console.log(v1 || "fallback"); // "fallback" — || can't tell 0 from "missing"
console.log(v2 ?? "fallback"); // "fallback"
|| falls back on any falsy value, so a real
0 or "" gets silently replaced.
?? only falls back on null and
undefined — reach for it whenever zero or empty string are
legitimate answers.
x == null is true for both null and
undefined, and false for everything else — a deliberate,
readable way to check "is this missing".
Numbers
There is only one number type: a 64-bit IEEE 754 double. No int,
no float distinction — 1 and 1.0 are the same
value, which is also why floating point gets weird.
console.log(0.1 + 0.2); // not 0.3
console.log(0.1 + 0.2 === 0.3); // false
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // the real check
Not a JavaScript bug — 0.1 has no exact binary form, the
same way 1/3 has no exact decimal form. Identical result in Java, C and
Python. Compare with a tolerance, and never store money in a float —
keep integer cents.
| Value | How you get it | How to detect it |
|---|---|---|
NaN |
0 / 0, Number("abc") |
Number.isNaN(v) — never === |
Infinity |
1 / 0 |
Number.isFinite(v) |
-0 |
literal -0, or -1 * 0 |
Object.is(v, -0) — -0 === 0 is true |
past MAX_SAFE_INTEGER |
253 and beyond |
Number.isSafeInteger(v) — use BigInt for
exact large integers
|
console.log(isNaN("abc")); // true — coerces first
console.log(Number.isNaN("abc")); // false — no coercion, the honest answer
console.log(Number.isNaN(NaN)); // true
The global isNaN coerces its argument before checking, so
it really answers "would this become NaN" — almost never the question
you meant. Number.isNaN does no coercion. Use it.
parseInt, parseFloat, toFixed
Three different parsers, not synonyms. Number() is strict —
the whole string or NaN. parseInt and
parseFloat are lenient: they read a prefix and stop at the
first character they don't understand.
console.log(Number("42px")); // NaN — not the whole string
console.log(parseInt("42px")); // 42 — reads a prefix
console.log(parseFloat("3.14em")); // 3.14
console.log(parseInt("0x1F")); // 31 — reads hex on its own
console.log(parseInt("08", 10)); // always pass the radix
["1","2","3"].map(parseInt) gives
[1, NaN, NaN]. map calls its function with
(value, index, array), so parseInt receives
the index as its radix — radix 1 is invalid, and radix 2 can't
read "3". Use .map(Number) or
.map(s => parseInt(s, 10)) instead.
console.log((1.005).toFixed(2)); // "1.00" — not "1.01"
console.log(typeof (1).toFixed(2)); // "string"
toFixed returns a string, and it rounds the double
that actually exists in memory — 1.005 is really
1.00499999…, so it rounds down. For display use
Intl.NumberFormat; for money, round integer cents yourself.
Strings
Immutable, and stored as UTF-16 code units — which is where
.length stops meaning "number of characters".
const name = "Ana", n = 3;
console.log(`Hi ${name}, you have ${n} item${n === 1 ? "" : "s"}`);
Any expression fits inside ${…} — ternaries, function
calls, even math. Multiline needs no \n — a real line
break inside the backticks is enough.
const s = "café 👍";
console.log(s.length); // 6 — code UNITS, not characters
console.log([...s].length); // 5 — code points
The 👍 is a surrogate pair — one character stored as two code
units. For plain ASCII this never shows up; the moment emoji or accented
characters appear, .length quietly lies.
slice(a, b) |
substring(a, b) |
|
|---|---|---|
| negative index | counts from the end | clamped to 0 |
| start > end | returns "" |
silently swaps the two |
Prefer slice — one consistent set of rules.
substr is deprecated.
const s = " Hello, World ";
console.log(s.trim());
console.log(s.trim().toUpperCase());
console.log(s.includes("World"));
console.log(s.trim().split(", "));
console.log(s.trim().replaceAll("o", "0"));
console.log(s.slice(2, 7));
Every one of these returns a new string. There is no in-place string operation in JavaScript.
| Sequence | Meaning |
|---|---|
\n |
newline |
\t |
tab |
\\ |
one literal backslash |
\" |
escaped quote — or just switch quote style |
\u00e9 |
→ é — 4 hex digits, one code unit |
Truthy / falsy — the eight
The falsy list is short and closed. Memorise these eight — everything else in the language is truthy.
Those eight are falsy. Everything else — including these commonly mistaken ones — is truthy:
console.log(!![]); // true — [] is not in the falsy list
console.log([] == false); // true — == turns BOTH sides into numbers: false→0, []→""→0
console.log([] === false); // false — different types, no coercion
Two unrelated mechanisms landing on opposite-looking answers for the
same value. if consults the falsy list; ==
runs a coercion algorithm.
Explicit conversion
Three functions, always called without new.
Converting on purpose is how you stop the language converting behind
your back.
| value | String(v) |
Number(v) |
Boolean(v) |
|---|---|---|---|
"" |
"" |
0 | false |
"12" |
"12" |
12 | true |
"12px" |
"12px" |
NaN | true |
null |
"null" |
0 | false |
undefined |
"undefined" |
NaN | false |
[] |
"" |
0 | true |
[5] |
"5" |
5 | true |
[1, 2] |
"1,2" |
NaN | true |
Object-to-primitive conversion runs Symbol.toPrimitive,
then valueOf, then toString — the entire
explanation for why [] + [] is "" and
[] + {} is "[object Object]".
console.log(String(null), Number(null), Boolean(null));
console.log(+"3.14"); // Number("3.14")
console.log(5 + ""); // String(5) — the lazy way
console.log(!!""); // Boolean("")
console.log([] + []);
console.log([] + {});
== vs ===
=== is one rule: same type and same value.
== is an algorithm — null == undefined is a
special case, and a mismatched type on either side gets converted
before comparing.
NaN vs NaN |
0 vs -0 |
coerces types? | |
|---|---|---|---|
== |
false | true | yes |
=== |
false | true | no |
Object.is |
true | false | no |
| SameValueZero | true | true | no |
SameValueZero is what Array.prototype.includes,
Map keys and Set members actually use — which
is why [NaN].includes(NaN) is true while
[NaN].indexOf(NaN) is -1
(indexOf uses ===).
console.log(0 == "0", 0 == "", "0" == "");
console.log(false == "false"); // false — "false" isn't the number 0
console.log(null == undefined); // true — the one special case
console.log(null === undefined); // false — different types
console.log([NaN].includes(NaN), [NaN].indexOf(NaN));
===. The one
accepted exception is x == null, which tests
null-or-undefined in a single check.
See what the callback captured
The same loop twice, one keyword apart. Watch how many bindings each version creates — that is the whole difference.
See the lookup walk
Watch the lookup walk. JavaScript does not copy methods onto objects; it walks a chain until it finds one, and stops at the first hit.
Opens in the editor — write it, run it, and check it against real tests.