Types, coercion and modules

javascript · memo

In one line: Seven primitives (immutable, by value) + object (by reference). Coercion runs a few spec algorithms — ToPrimitive, ToNumber, IsLooselyEqual — so every “WAT” is traceable. ESM imports are live bindings; require returns an object.

Download PDF Print view LaTeX source

Types, coercion and modules — figure 1

Types and equality

  • Primitives: string number bigint boolean undefined symbol null; everything else is an object (arrays, functions, dates). "a".length works by temporary boxing.
  • typeof: null → "object" (a 1995 tag bug, kept forever) · functions → "function" · arrays → "object" (Array.isArray) · undeclared → "undefined" (a TDZ name throws).
  • ===: no coercion. == (IsLooselyEqual): same type → ===; null == undefined and nothing else; number vs string → ToNumber; boolean → ToNumber first; object vs primitive → ToPrimitive; then repeat.
  • NaN !== NaN, +0 === -0. Object.is fixes both; SameValueZero (includes, Map/Set keys): NaN equals NaN, ±0 equal. indexOf(NaN) is -1, includes(NaN) true.
  • Falsy: false 0 -0 0n "" null undefined NaN — nothing else. "0", "false", [], {} are truthy.

The puzzles — and why

expressionresultwhy
[] + {}"[object Object]"both → strings ("", "[object Object]"), concat
{} + []0 (statement)leading {} is a block; +[] → 0
[] + []"""" + ""
[1,2] + [3]"1,23"arrays join with commas
"5" - 2 or "5" + 23 or "52"- is numeric only; + prefers strings
+"" · +" 12 " · +"12px"0 · 12 · NaNToNumber trims; whole string must parse
null + 1 · undefined + 11 · NaNnull → 0, undefined → NaN
null == 0 · null >= 0false · true== special-cases null; >= does ToNumber
[10,1,2].sort()[1,10,2]default sort compares strings
"b"+"a"+ +"a"+"a""baNaNa"+"a" is NaN
0.1 + 0.2 === 0.3false0.30000000000000004

Numbers

  • One number: IEEE-754 double. Integers exact up to Number.MAX_SAFE_INTEGER = 253-1; 2**53 + 1 === 2**53. Compare floats with a tolerance (Number.EPSILON = 2-52 is relative).
  • BigInt (10n, ES2020): arbitrary precision; mixing with number in arithmetic throws TypeError; JSON.stringify throws. IDs from a 64-bit backend → send as strings.
  • Bitwise ops go through Int32: 2**31 | 0 is negative.

??, ||, ?.

a || d takes d for any falsy a (so 0, "" are lost); a ?? d only for null/undefined (ES2020). Mixing ?? with ||/&& needs parens (SyntaxError). a?.b.c short-circuits the whole chain to undefined; f?.(), o?.[k]; not assignable. ??=, ||=, &&= (ES2021).

Value vs reference, copying

Primitives copy the value; objects copy the reference (“call by sharing”: mutating a param is visible to the caller, reassigning it is not). Swift: no structs here — every object behaves like a class, so value semantics are by convention (spread, freeze, immutable updates). {...o}, [...a], Object.assign are shallow. structuredClone is deep: cycles, Map/Set, Date, typed arrays; throws on functions; class instances come back as plain objects. JSON round-trip drops undefined/functions, turns Dates into strings, throws on cycles and BigInt.

Modules — ESM vs CommonJS

ESM import/exportCJS require/module.exports
whenstatic, top level, linked before run; hoisteda runtime call; anywhere, conditional
bindingslive, read-onlya plain object; destructuring copies
loadingasync-capable; top-level awaitsynchronous
this, modeundefined, always strictmodule.exports, sloppy
tree-shakingyes (static shape)hard

  • Circular: CJS gets the partially filled exports (missing keys undefined); ESM: a let/const/class read before its module ran throws ReferenceError (TDZ); function declarations work (hoisted).
  • Interop: import x from 'cjs' = module.exports; CJS named imports are guessed. require(esm) was ERR_REQUIRE_ESM (→ import()); bundlers/TS fake ESM via __esModule — the .default mess.
  • import() (both systems) → promise of the namespace: lazy/split loading.

Interview traps

  • count || 10 with count = 0 gives 10 — use ??.
  • typeof null is "object"; x != null checks both nullish.
  • Top-level await in a shared module blocks every importer’s evaluation.

Remember

=== by default, ?? for defaults · ToPrimitive then ToNumber · doubles lie after 253 · spread is shallow · ESM links, CJS runs.

Likely questions

  1. == vs === vs Object.is? — coerce · strict · strict + NaN, ±0.
  2. Why is [] == ![] true? — drawing 1.
  3. ESM vs CJS? — static + live + async-capable vs runtime + copied + sync.