B7 · Part 1 · Beginner
Types & values
Eight types, one conversion table, and the equality check that never stops mattering.
10On this page
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" |
The one rule that explains most bugs →
a primitive is copied whole; an object is only ever handed around by
reference. Two different variables can end up pointing at the
same object.
Primitive — the value itself is copied
let a10
let b = a10
two independent boxes
Object — only the reference is copied
let o1{ n: 10 }
let o2 = o1{ n: 10 }
both names point at the SAME object ↑
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.
The one accepted == exception
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
⚠ The map(parseInt) trap
["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 |
Tagged templates — a function that intercepts a template literal
function highlight(strings, ...values) {
return strings.reduce(
(out, str, i) => out + str + (values[i] !== undefined ? "**" + values[i] + "**" : ""),
""
);
}
const name = "Ana", score = 97;
console.log(highlight`Player ${name} scored ${score}`); // what happens?
"Player **Ana** scored **97**" — putting a function name
directly before a template literal, with no parentheses, doesn't
interpolate the string first and hand you the result: it calls
highlight with the literal pieces split apart —
["Player ", " scored ", ""] as strings, and
[name, score] as the individual values — so the function
decides how to combine them. This is exactly how CSS-in-JS libraries
(styled-components' styled.div`...`) and
SQL-templating libraries intercept a query to escape each
interpolated value before it ever touches a real query string.
console.log(String.raw`Line 1\nLine 2`); // what happens?
"Line 1\nLine 2", printed literally with the backslash
and the "n" still visible — not an actual line break.
String.raw is the one built-in tag, and it receives the
raw, unescaped source text (strings.raw instead of
strings), which is exactly why a Windows file path like
String.raw`C:\Users\ana` can be written without
doubling every backslash.
Truthy / falsy — the eight
The falsy list is short and closed. Memorise these eight — everything else in the language is truthy.
false
0
-0
0n
""
null
undefined
NaN
Those eight are falsy. Everything else — including these commonly mistaken ones — is truthy:
"0"
"false"
[]
{}
[0]
" "
-1
function(){}
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.
Dry run: coercing [] == false step by step
| Step | Comparison | Rule applied | Becomes |
|---|---|---|---|
| 1 | [] == false | one side is boolean → convert it with ToNumber(false) | [] == 0 |
| 2 | [] == 0 | object vs number → convert the object with ToPrimitive([]), which calls [].toString() | "" == 0 |
| 3 | "" == 0 | string vs number → convert the string with ToNumber("") | 0 == 0 |
| 4 | 0 == 0 | same type, same value | true |
Three separate conversions run before == ever compares
two values of the same type — [] never gets compared to
false directly. === skips every one of
these steps and returns false the instant it sees two
different types.
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 rule Always
===. The one
accepted exception is x == null, which tests
null-or-undefined in a single check.
Interview answer, one sentence
"JavaScript has seven primitives plus object; typeof null lies, coercion makes == unpredictable so use ===, and exactly eight values are falsy: false, 0, -0, 0n, the empty string, null, undefined and NaN."
Before you move on
- List the eight types, and explain what "copied by value" versus "copied by reference" actually means for a primitive versus an object.
- Explain why
typeof nullis"object", and what to check instead. - Walk
[] == falsethrough its coercion steps by hand, and say why[] === falseskips all of them. - Recite the eight falsy values from memory, and name three commonly-mistaken truthy values.
- Explain when
??gives a different answer from||, with a concrete value where it matters.
Opens in the editor: write it, run it, and check it against real tests.
All 35 for this level →Finished reading?
0 of 41 chapters read in this series