Values and Types
This chapter assumes you have written some JavaScript before and are comfortable reading code with functions and objects. Our goal here is not "what a string is" but how JavaScript's type system actually works: the distinction between primitives and objects, where coercion is specified versus accidental, and how equality operators resolve at the engine level.
The Seven (Plus One) Primitive Types
JavaScript's runtime has a fixed set of primitive types:
| Type | Example | Notable behavior |
|---|---|---|
| `number` | `42`, `3.14`, `-0`, `NaN`, `Infinity` | IEEE 754 double precision; one type for int and float |
| `string` | `"hello"` | Immutable sequence of UTF-16 code units |
| `boolean` | `true`, `false` | Exactly two values |
| `undefined` | `undefined` | Missing value; own type and value |
| `null` | `null` | Intentional absence; `typeof null === "object"` (a legacy bug) |
| `symbol` | `Symbol('id')` | Unique, non-string object keys |
| `bigint` | `9007199254740993n` | Arbitrary precision integers, cannot mix with `number` |
The newer specification adds objects as the catch-all eighth type, with functions as callable objects. The critical structural fact: primitives are immutable and compared by value; objects are mutable and compared by identity.
let a = "hi";
a[0] = "H"; // silently ignored: strings are immutable
console.log(a); // "hi"
const x = { v: 1 };
const y = x;
y.v = 2;
console.log(x.v); // 2 — same reference, not a copyPrimitives are also not objects, yet you can call methods on them. The engine wraps the primitive in a temporary object (a "boxed" wrapper: String, Number, Boolean), reads the property, and discards the wrapper.
const len = "hello".length; // 5 — works via transient String wrapper
"use strict";
x = 5;
x.foo = 1; // TypeError in strict mode; silently lost otherwise
console.log(x.foo); // undefined — the wrapper was thrown awayType Detection and Its Failure Modes
typeof is the default tool, but know its edge cases:
typeof null; // "object" — historical bug, never fixed (breaking the web)
typeof []; // "object" — arrays are not distinguished
typeof function(){}; // "function" — a special [[Call]] internal slot
typeof NaN; // "number" — NaN is a number that fails self-equality
typeof undeclaredVar; // "undefined" — safe on undeclared identifiers
typeof 10n; // "bigint"Reliable alternatives where typeof fails:
Array.isArray([]); // true
Object.prototype.toString.call(null); // "[object Null]" — the robust internal check
Number.isNaN(NaN); // true — unlike global isNaN, no coercion
Object.is(-0, 0); // false — distinguishes signed zeroNaN deserves emphasis: NaN === NaN is false by IEEE 754 specification. Detect it with Number.isNaN(value), which does not coerce its argument — unlike the global isNaN, which converts first (isNaN("foo") is true).
Coercion: Explicit vs Implicit
Coercion is conversion between types. Explicit coercion uses String(), Number(), Boolean(). Implicit coercion happens via operators and contexts, and is the source of most type-system confusion.
The rules that matter:
- To number:
null → 0,undefined → NaN,true → 1,"" → 0, non-numeric strings →NaN. Objects go throughToPrimitive(see below). - To boolean: exactly seven values are falsy —
false,0,-0,0n,"",null,undefined,NaN. Everything else, including[],{}, and"0", is truthy. +prefers strings: if either operand is a string (afterToPrimitive), the other is coerced to string.- Arithmetic operators (
-,*,/) force numbers.
1 + "2"; // "12" (+ sees a string, concatenates)
"5" - 2; // 3 (- forces numeric)
[] + []; // "" (ToPrimitive of [] is "", join of empties)
[] + {}; // "[object Object]"
0 == ""; // true (both coerce to 0)
null == undefined; // true (spec-defined pair)
null == 0; // false (null only equals undefined/objects — special case)
[] == false; // true ([] → "" → 0; false → 0)The [] == false case is worth tracing in the spec's terms: abstract equality applies ToPrimitive to both sides, then numeric comparison. Empty array → "" → 0; false → 0; 0 == 0 → true. This is why the professional norm is: always use ===, except in the two accepted cases — x == null to check for both null and undefined at once.
ToPrimitive and ValueOf/ToString
When an object is coerced, the engine calls its Symbol.toPrimitive hint method if present; otherwise valueOf(), then toString(), in an order set by the hint ("number", "string", or "default"). The + operator uses the default hint, and ordinary objects define valueOf as returning themselves, so toString wins.
const price = {
cents: 1250,
[Symbol.toPrimitive](hint) {
return hint === "number" ? this.cents : `${(this.cents / 100).toFixed(2)}`;
},
};
console.log(+price); // 1250 (number hint)
console.log(`${price}`); // "12.50" (string hint)Symbol primitives deliberately cannot coerce to string implicitly — Symbol() + "" throws — to prevent accidental symbol-key collisions in string contexts.
How Values Live in Memory
The diagram below shows that variables hold either the value itself (primitives) or a reference to a heap object:
Two consequences follow. First, copying primitives copies values; copying references shares one object — a common source of "mutation leak" bugs. Second, primitives never participate in the garbage-collection graph; only objects are allocated and collected (covered in the memory chapter).
Why This Matters
Nearly every later topic in this tutorial builds on these mechanics: == vs === behavior in default parameters, ToPrimitive in template literals, boxing in property access, identity in Map keys. Treat the type system as specified behavior rather than accumulated accidents — most of it is precisely defined, and the defined behavior is predictable once you know the coercion algorithm.
Exercise
Trace why {} == {} is false but "" == "" is true, in terms of the spec's comparison algorithms.
Show answer
String comparison is by value: identical strings compare equal because their code units match. Object comparison uses the SameValue / strict-equality algorithm, which compares references, not contents. Two distinct object literals are two distinct allocations, so their references differ and the comparison is false regardless of structure.
Exercise
You need a function isEmpty(x) that returns true for null, undefined, "", 0, and NaN, and false for [], {}, and "0". Why can you not express this with a single == comparison?
Show answer
The falsy set includes [] and {} under boolean coercion, but they are truthy for your rule, so !x fails. And NaN === NaN is false, so identity checks fail too. You must check explicitly: x == null || x === "" || x === 0 || Number.isNaN(x) — combining abstract equality for the null pair, strict equality for values, and Number.isNaN for NaN.
Spotted a mistake, or code that does not work?