Topics
v5.0.0
On this page · sections

JavaScript Types, Coercion & Equality

Can you predict what JavaScript considers equal — and recognize when that behavior becomes unsafe in production?

JavaScript’s type system looks simple until an interviewer asks about:

typeof null [] == ![] NaN === NaN Object.is(-0, 0)

These questions are rarely about memorizing strange outputs.

They test whether you understand:

  • Primitive values vs object identity
  • typeof and its limitations
  • Strict equality vs abstract equality
  • Object.is and SameValueZero
  • ToPrimitive and implicit coercion
  • Truthiness vs actual existence
  • When coercion is useful at application boundaries
  • When implicit coercion becomes a production bug

The senior-level mental model is:

Value │ ├── Primitive │ ├── string │ ├── number │ ├── bigint │ ├── boolean │ ├── symbol │ ├── undefined │ └── null │ └── Object ├── Array ├── Function ├── Date ├── Map └── ...

Then, when comparing values:

Comparison ↓ Which equality algorithm? │ ├── === ├── Object.is ├── SameValueZero └── == ↓ May perform coercion

The key skill is not memorizing weird expressions. It is knowing which algorithm JavaScript is applying and why.

Questions

Types

  1. typeof and the null Anomaly
  2. Primitives vs Object Identity

Equality

  1. Strict vs Loose Equality
  2. Object.is and SameValueZero

Senior-Level Follow-ups

  1. Why [] == ![] Is true
  2. Truthy, Falsy, and Existence Checks
  3. When Is Coercion a Feature?

Types

1. typeof and the null Anomaly

Why This Matters

typeof is useful for quick primitive checks, but it is not a complete runtime type inspection mechanism.

The classic interview trap is:

typeof null; // "object"

This is a historical behavior of JavaScript that cannot be changed without breaking existing code.

Problem

What does typeof return for:

  • null
  • arrays
  • functions
  • NaN
  • objects

Example

typeof null; // "object" typeof []; // "object" typeof {}; // "object" typeof function () {}; // "function" typeof NaN; // "number"

The last result is important:

typeof NaN === "number"; // true

NaN is a special numeric value. It represents an invalid numeric result, but it still belongs to JavaScript’s number type.

Why Is typeof null "object"?

Historically, JavaScript represented values using internal type information where null ended up sharing the object classification used by typeof.

The behavior became part of the language’s compatibility surface.

So don’t explain it as:

“null is an object.”

That’s incorrect.

Instead:

“null is a primitive value, but typeof null returns 'object' for historical compatibility.”

Arrays Are Objects

typeof []; // "object"

That’s because arrays are objects with special array semantics.

Therefore:

typeof value === "object"

does not tell you whether value is:

  • a plain object
  • an array
  • a date
  • a regular expression
  • null
  • another built-in object

If you need to distinguish these values, use a more appropriate mechanism.

Object.prototype.toString

For cross-realm-friendly built-in tagging:

Object.prototype.toString.call([]); // "[object Array]" Object.prototype.toString.call(new Date()); // "[object Date]" Object.prototype.toString.call(null); // "[object Null]"

This can be useful when values may originate from another JavaScript realm, such as an iframe.

Why instanceof Can Be Tricky

Consider:

value instanceof Array

This checks whether Array.prototype from the current realm appears in the object’s prototype chain.

Different realms have different intrinsic constructors.

For example:

Main window └── Array iframe └── Array

Those Array constructors are distinct objects.

Therefore an array created inside an iframe can fail:

iframeArray instanceof Array // false

even though it is genuinely an array.

For modern application code, use the check that matches the requirement:

Array.isArray(value);

for arrays, or:

Object.prototype.toString.call(value);

when you specifically need a built-in tag.

Interview Answer

“typeof is useful but incomplete. null is a primitive even though typeof null returns 'object' for historical reasons. Arrays are objects, and functions are callable objects for which typeof returns 'function'. For arrays I use Array.isArray(), and for cross-realm built-in tagging Object.prototype.toString.call() can be more appropriate than instanceof.”

2. Primitives vs Object Identity

Why This Matters

The phrase “objects are passed by reference” is common, but it is an imprecise explanation of JavaScript semantics.

A better model is:

Variables contain values. For objects, that value is a reference to an object.

This distinction becomes important when reasoning about:

  • assignment
  • equality
  • mutation
  • function arguments
  • shallow copies
  • state updates

Primitive Values

let a = 1; let b = a; b = 2; console.log(a); // 1 console.log(b); // 2

Changing b does not change a.

Conceptually:

a → 1 b → 1 b = 2 a → 1 b → 2

Object Values

const x = { name: "Ada", }; const y = x; y.name = "Grace"; console.log(x.name); // "Grace"

Both variables contain references to the same object:

x ─────┐ ↓ Object ↑ y ─────┘

Therefore mutation through either reference is observable through the other.

Object Equality Is Identity-Based

This is one of the most important consequences:

{} === {}; // false

Each object literal creates a different object.

Even if their contents are identical:

const a = { id: 1 }; const b = { id: 1 }; a === b; // false

because:

a → Object A b → Object B

The objects have equal-looking contents but different identities.

Shallow Copies

Object spread creates a new outer object:

const original = { name: "Ada", settings: { theme: "dark", }, }; const copy = { ...original, };

Now:

copy === original; // false

but:

copy.settings === original.settings; // true

The result is:

original ─────→ Object A │ └── settings ───→ Object C copy ──────────→ Object B │ └── settings ───→ Object C

So spread is a shallow copy.

The same principle applies to:

Object.assign({}, original);

Function Arguments

JavaScript passes arguments by value.

For an object, the value being passed is the object reference.

function update(user) { user.name = "Grace"; } const user = { name: "Ada", }; update(user); console.log(user.name); // "Grace"

The function received a copy of the reference pointing to the same object.

However:

function replace(user) { user = { name: "Grace", }; } const user = { name: "Ada", }; replace(user); console.log(user.name); // "Ada"

Reassigning the local parameter does not replace the caller’s binding.

Interview Answer

“JavaScript variables hold values. For objects, that value is a reference to an object, so copying the variable copies the reference and both variables can observe mutations to the same object. Object equality is identity-based, and spread or Object.assign only create shallow copies. Function arguments are passed by value; for objects, the passed value is the object reference.”

Equality

3. Strict vs Loose Equality

Why This Matters

JavaScript has multiple equality algorithms.

The two most commonly encountered operators are:

=== ==

They are not merely “strict” and “loose” versions of the same operation.

=== performs Strict Equality Comparison.

== performs Abstract Equality Comparison, which can perform type conversion before comparing.

Strict Equality: ===

Examples:

1 === 1; // true 1 === "1"; // false 0 === false; // false null === undefined; // false

For primitives, strict equality generally requires compatible types and equal values.

For objects, identity matters:

{} === {}; // false

Loose Equality: ==

Loose equality can coerce values:

0 == false; // true "" == false; // true "1" == 1; // true null == undefined; // true

But the coercion rules are not symmetrical in the intuitive sense.

For example:

null == 0; // false

and:

null == undefined; // true

This is why trying to reason about == using informal statements such as:

“JavaScript converts everything to a string.”

does not work.

Different equality cases invoke different abstract operations.

The Useful == null Case

There is one widely accepted practical use:

value == null

This intentionally matches both:

null undefined

but not ordinary values such as:

0 false ""

Equivalent explicit code:

value === null || value === undefined

Whether to allow == null should follow the project’s linting and style conventions.

In many codebases, === is preferred everywhere for consistency.

Don’t Compare Objects With == to Compare Their Contents

This does not perform deep equality:

{ id: 1 } == { id: 1 }; // false

It is still comparing different object identities.

If you need structural equality, use an explicit strategy appropriate to the data:

Object identity → === / Object.is Primitive value equality → === / Object.is Collection membership → includes / Set / Map Structural equality → explicit deep comparison

Interview Answer

“=== performs strict equality without the type coercion performed by ==. I use === by default because it makes type conversions explicit. The one common intentional use of == is value == null when I specifically want to match both null and undefined. I would never use == as a substitute for structural equality.”

4. Object.is and SameValueZero

Why This Matters

=== is not the only equality algorithm in JavaScript.

Two values expose why:

NaN -0

NaN

NaN === NaN; // false

But:

Object.is(NaN, NaN); // true

Signed Zero

JavaScript numbers distinguish -0 and +0 in certain operations:

Object.is(-0, 0); // false

while:

-0 === 0; // true

So the equality algorithms differ.

Three Important Algorithms

AlgorithmNaN vs NaN-0 vs +0Typical usage
Strict Equality (===)DifferentSameGeneral application comparisons
SameValue (Object.is)SameDifferentPrecise value identity semantics
SameValueZeroSameSameMap, Set, includes

A useful mental model:

=== NaN !== NaN -0 === +0
Object.is() NaN === NaN -0 !== +0
SameValueZero NaN === NaN -0 === +0

includes vs indexOf

This difference is an excellent interview example:

[NaN].includes(NaN); // true

but:

[NaN].indexOf(NaN); // -1

Why?

includes() uses SameValueZero semantics.

indexOf() uses strict equality semantics.

Map and Set

SameValueZero also explains:

const set = new Set(); set.add(NaN); set.has(NaN); // true

and:

const map = new Map(); map.set(NaN, "value"); map.get(NaN); // "value"

These data structures need an equality model suitable for key/member lookup, so their semantics differ from ===.

React Nuance

Some React comparison paths use Object.is semantics. This is why differences involving:

NaN -0

can matter when discussing React’s value comparisons.

Don’t generalize this into:

“React always uses Object.is.”

The correct answer is:

“React uses Object.is semantics in specific equality checks, such as dependency/value comparisons where documented by the API.”

Interview Answer

“Object.is uses SameValue semantics: NaN equals itself and -0 differs from +0. SameValueZero also treats NaN as equal to itself but considers -0 and +0 equal. Map, Set, and Array.prototype.includes() use SameValueZero semantics, while indexOf() uses strict equality.”

Senior-Level Follow-ups

5. Why [] == ![] Is true

This is a classic coercion puzzle:

[] == ![]; // true

Don’t memorize the result. Walk through the conversions.

Step 1: Evaluate ![]

Arrays are objects, and objects are truthy:

Boolean([]); // true

Therefore:

![]; // false

So the expression becomes:

[] == false

Step 2: Compare Array and Boolean

For this loose-equality comparison, the boolean is converted to a number:

Number(false); // 0

Conceptually:

[] == 0

Step 3: Convert the Array

The array is an object, so the comparison needs a primitive representation.

This invokes the ToPrimitive process.

For an empty array, the resulting primitive string is:

String([]); // ""

Then:

Number(""); // 0

So we arrive at:

0 == 0

which is:

true

The Mental Model

[] ↓ truthy ↓ ![] → false ↓ [] == false ↓ false → 0 ↓ [] → "" → 0 ↓ 0 == 0 ↓ true

The important lesson is not the individual conversions.

It’s:

== can trigger a chain of implicit conversions, including ToPrimitive, which makes seemingly unrelated values comparable.

Interview Answer

“[] is truthy, so ![] becomes false. Loose equality then converts the boolean to 0, and the empty array is converted through ToPrimitive to "", which converts to 0. The comparison becomes 0 == 0, so the result is true. I don’t memorize these puzzles; I trace the abstract equality and coercion steps.”

6. Truthy, Falsy, and Existence Checks

Why This Matters

Truthy/falsy checks are convenient, but they answer a different question from:

“Does this value exist?”

JavaScript has a small set of falsy values.

false 0 -0 0n "" null undefined NaN

Everything else is truthy.

That includes:

[] {} "0" "false"

The count Bug

Consider:

const count = 0; if (count) { // does not run }

If the business meaning is:

“Does count have a value?”

then truthiness is the wrong test.

Instead, decide what “exists” means.

Nullish Check

if (count != null) { // accepts 0 }

or explicitly:

if (count !== null && count !== undefined) { // accepts 0 }

Numeric Validation

If you need a valid finite number:

if (Number.isFinite(count)) { // accepts 0 }

Map / Cache Existence

Don’t do:

if (cache.get(key)) { // ... }

because a legitimate cached value might be falsy:

cache.set(key, 0);

Instead:

if (cache.has(key)) { const value = cache.get(key); }

This expresses the actual semantic question:

Does this key exist?

rather than:

Is the stored value truthy?

|| vs ??

This distinction appears frequently in frontend code.

const page = input || 1;

treats all falsy values as missing.

So:

0 || 1; // 1

Nullish coalescing is more precise:

const page = input ?? 1;

Now:

0 ?? 1; // 0

because ?? only falls back for:

null undefined

Interview Answer

“Truthiness is not the same as existence. 0, false, and '' can all be legitimate values. I choose the check based on the domain: value != null for nullish presence, Number.isFinite() for numeric validity, and Map.has() when checking whether a key exists. I use ?? rather than || when only null and undefined should trigger a fallback.”

7. When Is Coercion a Feature?

The senior-level answer is not:

“Never use coercion.”

Coercion is useful when it is intentional, explicit, and localized.

The problem is primarily implicit coercion inside domain logic.

Good: Explicit Boundary Conversion

Form values arrive as strings:

const age = Number(formValue);

The conversion is explicit and occurs at the boundary between external data and application logic.

For integer parsing:

const page = Number.parseInt(input, 10);

followed by appropriate validation if malformed input matters.

Good: Explicit Stringification

If the requirement is to create a string:

const label = String(value);

This is clearer than relying on:

value + ""

The intent is visible.

Sometimes Useful: value == null

As discussed earlier:

value == null

can intentionally mean:

null OR undefined

This is one of the few cases where loose equality can communicate useful semantics.

Whether your codebase permits it should be governed by lint rules and team conventions.

Dangerous: Implicit Coercion in Domain Logic

Avoid logic such as:

if (user.id == request.id) { // ... }

if the IDs can legitimately arrive as different types.

For example:

42 == "42"; // true

A better approach is to normalize at the boundary:

const requestId = String(request.id); const userId = String(user.id); if (userId === requestId) { // ... }

Now the domain comparison is explicit.

Dangerous: Objects as String Keys

This is especially important for caches.

Consider:

const cache = new Map(); cache.set(String({ id: 1 }), "first"); cache.set(String({ id: 2 }), "second");

Both objects stringify to:

"[object Object]"

So the second operation overwrites the first.

The problem is not merely “coercion is weird.”

The real problem is:

The conversion destroyed information required to distinguish the values.

Prefer a stable domain key:

const key = `user:${user.id}`;

or use the object itself as a Map key when identity is what you actually want:

const cache = new Map(); cache.set(userObject, result);

Coercion Strategy

A good production rule is:

External boundary ↓ Normalize ↓ Validate ↓ Domain logic ↓ Use explicit types ↓ Compare intentionally

For example:

function normalizeUserId(input) { const id = String(input); if (!id) { throw new Error("Invalid user ID"); } return id; }

Then downstream code can safely assume:

user.id === request.id

because both values have already been normalized.

Interview Answer

“Coercion is useful at boundaries where I intentionally normalize external data, such as converting form input to numbers or IDs to strings. Inside domain logic I prefer explicit types and explicit comparisons. The biggest risk is not coercion itself but losing information or silently changing semantics—for example, turning different objects into the same string cache key.”

Final Interview Strategy

When you encounter a JavaScript type or equality question, use this sequence:

1. What kind of value is this? ↓ 2. Primitive or object? ↓ 3. If object, does identity matter? ↓ 4. Which equality algorithm is being used? ↓ 5. Does the algorithm perform coercion? ↓ 6. Which abstract operation performs the conversion? ↓ 7. Is truthiness being confused with existence? ↓ 8. Would this behavior be safe in production?

Quick Reference

typeof ├── null → "object" (historical behavior) ├── [] → "object" ├── {} → "object" ├── function → "function" └── NaN → "number" === ├── no implicit type coercion ├── primitive values compared by value └── objects compared by identity == ├── Abstract Equality Comparison ├── may coerce operands └── value == null → null OR undefined Object.is() ├── NaN equals NaN └── -0 differs from +0 SameValueZero ├── NaN equals NaN └── -0 equals +0 Truthiness ├── 0 is falsy ├── "" is falsy ├── false is falsy ├── null / undefined are falsy ├── [] is truthy └── {} is truthy Coercion ├── useful at boundaries ├── normalize before domain logic └── dangerous when it silently destroys information

Senior-Level Signal

A weak candidate memorizes:

[] == ![]; // true

A strong candidate explains why.

An even stronger candidate explains when the behavior matters architecturally:

“typeof is a coarse type check, not a complete type system. Objects are compared by identity rather than structure. ===, Object.is, and SameValueZero intentionally have different semantics, while == may perform multiple coercions through abstract operations such as ToPrimitive. In production, I normalize external values at boundaries and keep domain comparisons explicit. Truthiness is also not an existence check, so APIs such as Map.has() and ?? are often more semantically correct than relying on coercion or falsy checks.”

That is the level of answer that moves the discussion from JavaScript trivia to engineering judgment.

Last updated on