Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Beginner/B7
7 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSB7 · Part 1 · Beginner

Types & values

Eight types, one conversion table, and the equality check that never stops mattering.

9 min read10 sections3 exercises
7 of 41

In this chapter

  • Seven primitives + object
  • typeof — and its two lies
  • null vs undefined
  • Numbers
  • parseInt, parseFloat, toFixed

and 5 more

about 9 min
10On this page
  1. Seven primitives + object
  2. typeof — and its two lies
  3. null vs undefined
  4. Numbers
  5. parseInt, parseFloat, toFixed
  6. Strings
  7. Tagged templates — a function that intercepts a template literal
  8. Truthy / falsy — the eight
  9. Explicit conversion
  10. == vs ===

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.
Copy by value vs copy by reference
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

StepComparisonRule appliedBecomes
1[] == falseone side is boolean → convert it with ToNumber(false)[] == 0
2[] == 0object vs number → convert the object with ToPrimitive([]), which calls [].toString()"" == 0
3"" == 0string vs number → convert the string with ToNumber("")0 == 0
40 == 0same type, same valuetrue

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 null is "object", and what to check instead.
  • Walk [] == false through its coercion steps by hand, and say why [] === false skips 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.

03Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 9 testsFix typeof's two liesbeginner
  • 10 testsKeep only the truthy valuesbeginner
  • 10 testsCopy without sharingbeginner
All 35 for this level →
B7

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. B6Previous · B6Inside the browser
  2. B7you are here
  3. Next · B8Strings, numbers & MathThe everyday toolbox every other chapter quietly assumes you already have.5 min read

01On this page

10 sections · 0%

  1. 01Seven primitives + object
  2. 02typeof — and its two lies
  3. 03null vs undefined
  4. 04Numbers
  5. 05parseInt, parseFloat, toFixed
  6. 06Strings
  7. 07Tagged templates — a function that intercepts a template literal
  8. 08Truthy / falsy — the eight
  9. 09Explicit conversion
  10. 10== vs ===

[] chapters · t top