Data types

Updated 7 interview angles 5 min read source
On this page10
  1. The primitives
  2. typeof lies twice
  3. NaN, the value that is not itself
  4. Falsy values — all eight
  5. == versus ===
  6. Copying: shallow, deep, and the modern answer
  7. Symbol and BigInt
  8. var, let, const
  9. Related
  10. Interview angle

Data types

Seven primitives and everything else is an object. The interview value is in the edges: what typeof lies about, how coercion decides, and why copying is harder than it looks.

The primitives

string number boolean null undefined symbol bigint

Primitives are immutable and compared by value; everything else — objects, arrays, functions, dates, Map, Set — is compared by reference:

js
"abc" === "abc";          // true
[1] === [1];              // false — different objects
const a = { n: 1 }, b = a;
b.n = 2;  a.n;            // 2 — same object

null versus undefined is the pair to state precisely: undefined means “no value has been assigned”, null means “assigned, deliberately, to nothing”. A missing property, an omitted argument and a function with no return all give undefined. null only appears because someone wrote it.

typeof lies twice

js
typeof null;              // "object"  — a bug from 1995, unfixable
typeof [];                // "object"  — no distinct array type
typeof function () {};    // "function"
typeof NaN;               // "number"
typeof 10n;               // "bigint"

So typeof is only reliable for primitives other than null. The correct checks:

js
Array.isArray(x);                     // arrays
x === null;                           // null
Number.isNaN(x);                      // NaN, without coercion
Number.isInteger(x);                  // integers
Object.prototype.toString.call(x);    // "[object Date]" etc.

Number.isNaN over the global isNaN: the global coerces first, so isNaN("abc") is true even though a string is not NaN.

NaN, the value that is not itself

js
NaN === NaN;              // false — the only value unequal to itself
[NaN].includes(NaN);      // true  — SameValueZero
[NaN].indexOf(NaN);       // -1    — strict equality
Object.is(NaN, NaN);      // true

Three equality algorithms are in play. === says no, Object.is and SameValueZero (used by includes, Map, Set) say yes. That is why Data structures can dedupe NaN and indexOf cannot.

Object.is also separates 0 and -0, which === does not.

Falsy values — all eight

false 0 -0 0n "" null undefined NaN

Everything else is truthy, including [] and {}, which is the one that catches people:

js
if ([]) { }               // runs
[] == false;              // true  — coercion, a different question

The practical consequence is that if (value) is wrong whenever 0 or "" are legitimate values:

js
if (count) { }                  // skips a valid 0
if (count != null) { }          // catches null AND undefined, nothing else

const n = input ?? 10;          // only null/undefined fall through
const n = input || 10;          // 0 and "" also fall through — usually a bug

?? over || for defaults is the modern rule, and this is why.

== versus ===

=== compares type then value, no coercion. == coerces, and the rules are worth knowing only so you can avoid them:

js
1 == "1";                 // true  — string to number
null == undefined;        // true  — and to nothing else
null == 0;                // false — null is not coerced to a number
[] == false;              // true  — [] -> "" -> 0
"" == 0;                  // true

Use === always, with one accepted exception: x == null as a shorthand for “null or undefined”, which is idiomatic and reads well.

Copying: shallow, deep, and the modern answer

js
// one level only
const shallow = { ...obj };
const shallow2 = Object.assign({}, obj); // same

// the built-in answer
const deep = structuredClone(obj);

Spread copies the top level; nested objects are still shared, so mutating copy.nested.x changes the original. structuredClone is the built-in deep copy and it handles what JSON.parse(JSON.stringify(x)) cannot:

JSON round-trip structuredClone
Date becomes a string stays a Date
Map / Set becomes {} preserved
undefined values dropped preserved
Cyclic references throws handled
Functions dropped throws

The JSON trick is still everywhere in older code; naming structuredClone as its replacement is a small currency signal.

Symbol and BigInt

js
const id = Symbol("id");
obj[id] = 1;
// [] — symbol keys are hidden from most APIs
Object.keys(obj);

// well-known symbols hook into the language
Symbol.iterator;

A symbol is a guaranteed-unique key. Its real use is metadata that must not collide with, or be seen by, ordinary property enumeration — and the well-known symbols (Symbol.iterator, Symbol.asyncIterator) are how you make your own type work with for...of and spread.

js
9007199254740993n;               // exact; a Number cannot hold this
10n + 5n;                        // 15n
10n + 5;                         // TypeError — no mixing

BigInt exists because Number is a float64 and loses integer precision past Number.MAX_SAFE_INTEGER (2⁵³−1). Database ids and money in minor units are the usual reasons — and note that JSON.stringify throws on a BigInt, so serialising means converting to a string first.

var, let, const

Scope Hoisting Reassign
var function to undefined yes
let block temporal dead zone yes
const block temporal dead zone no

const prevents rebinding, not mutationconst a = []; a.push(1) is fine. Use const by default, let when you must reassign, and var never; the loop-closure consequence is in Closures.

Interview angle 7

  • “What does typeof null return and why?” - "object", a bug from the first implementation that cannot be fixed without breaking the web. typeof is only reliable for primitives other than null; use Array.isArray, x === null and Object.prototype.toString.call for the rest.
  • null or undefined?” - undefined means no value was assigned — a missing property, an omitted argument, a bare return. null is an assigned absence, so it only appears because someone wrote it.
  • “How do you check for NaN?” - Number.isNaN, not the global isNaN, which coerces first and so calls "abc" a NaN. NaN === NaN is false because it is the only value unequal to itself; Object.is and includes say true because they use different equality algorithms.
  • “Name the falsy values.” - false, 0, -0, 0n, "", null, undefined, NaN. Note that [] and {} are truthy, and that if (value) is wrong whenever 0 or "" are legitimate — which is the argument for ?? over ||.
  • == or ===?” - === always, with x == null as the one accepted exception because it catches null and undefined and nothing else. == coercion rules are worth knowing only so you can avoid relying on them.
  • “How do you deep copy an object?” - structuredClone. It preserves Dates, Maps, Sets, undefined values and cycles, all of which the JSON.parse(JSON.stringify()) trick loses or throws on. It does throw on functions.
  • “When would you use BigInt?” - when integers exceed Number.MAX_SAFE_INTEGER, which is 2⁵³−1 because Number is a float64. Database ids and money in minor units. You cannot mix BigInt and Number in arithmetic, and JSON.stringify throws on one.