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
typeofand its limitations- Strict equality vs abstract equality
Object.isand SameValueZeroToPrimitiveand 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 coercionThe key skill is not memorizing weird expressions. It is knowing which algorithm JavaScript is applying and why.
Questions
Types
Equality
Senior-Level Follow-ups
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";
// trueNaN 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:
“
nullis an object.”
That’s incorrect.
Instead:
“
nullis a primitive value, buttypeof nullreturns'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 ArrayThis 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
└── ArrayThose Array constructors are distinct objects.
Therefore an array created inside an iframe can fail:
iframeArray instanceof Array
// falseeven 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
“
typeofis useful but incomplete.nullis a primitive even thoughtypeof nullreturns'object'for historical reasons. Arrays are objects, and functions are callable objects for whichtypeofreturns'function'. For arrays I useArray.isArray(), and for cross-realm built-in taggingObject.prototype.toString.call()can be more appropriate thaninstanceof.”
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);
// 2Changing b does not change a.
Conceptually:
a → 1
b → 1
b = 2
a → 1
b → 2Object 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:
{} === {};
// falseEach object literal creates a different object.
Even if their contents are identical:
const a = { id: 1 };
const b = { id: 1 };
a === b;
// falsebecause:
a → Object A
b → Object BThe 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;
// falsebut:
copy.settings === original.settings;
// trueThe result is:
original ─────→ Object A
│
└── settings ───→ Object C
copy ──────────→ Object B
│
└── settings ───→ Object CSo 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.assignonly 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;
// falseFor primitives, strict equality generally requires compatible types and equal values.
For objects, identity matters:
{} === {};
// falseLoose Equality: ==
Loose equality can coerce values:
0 == false;
// true
"" == false;
// true
"1" == 1;
// true
null == undefined;
// trueBut the coercion rules are not symmetrical in the intuitive sense.
For example:
null == 0;
// falseand:
null == undefined;
// trueThis 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 == nullThis intentionally matches both:
null
undefinedbut not ordinary values such as:
0
false
""Equivalent explicit code:
value === null || value === undefinedWhether 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 };
// falseIt 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 comparisonInterview 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==isvalue == nullwhen I specifically want to match bothnullandundefined. 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
-0NaN
NaN === NaN;
// falseBut:
Object.is(NaN, NaN);
// trueSigned Zero
JavaScript numbers distinguish -0 and +0 in certain operations:
Object.is(-0, 0);
// falsewhile:
-0 === 0;
// trueSo the equality algorithms differ.
Three Important Algorithms
| Algorithm | NaN vs NaN | -0 vs +0 | Typical usage |
|---|---|---|---|
Strict Equality (===) | Different | Same | General application comparisons |
SameValue (Object.is) | Same | Different | Precise value identity semantics |
| SameValueZero | Same | Same | Map, Set, includes |
A useful mental model:
===
NaN !== NaN
-0 === +0Object.is()
NaN === NaN
-0 !== +0SameValueZero
NaN === NaN
-0 === +0includes vs indexOf
This difference is an excellent interview example:
[NaN].includes(NaN);
// truebut:
[NaN].indexOf(NaN);
// -1Why?
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);
// trueand:
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
-0can matter when discussing React’s value comparisons.
Don’t generalize this into:
“React always uses
Object.is.”
The correct answer is:
“React uses
Object.issemantics in specific equality checks, such as dependency/value comparisons where documented by the API.”
Interview Answer
“
Object.isuses SameValue semantics:NaNequals itself and-0differs from+0. SameValueZero also treatsNaNas equal to itself but considers-0and+0equal.Map,Set, andArray.prototype.includes()use SameValueZero semantics, whileindexOf()uses strict equality.”
Senior-Level Follow-ups
5. Why [] == ![] Is true
This is a classic coercion puzzle:
[] == ![];
// trueDon’t memorize the result. Walk through the conversions.
Step 1: Evaluate ![]
Arrays are objects, and objects are truthy:
Boolean([]);
// trueTherefore:
![];
// falseSo the expression becomes:
[] == falseStep 2: Compare Array and Boolean
For this loose-equality comparison, the boolean is converted to a number:
Number(false);
// 0Conceptually:
[] == 0Step 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("");
// 0So we arrive at:
0 == 0which is:
trueThe Mental Model
[]
↓
truthy
↓
![] → false
↓
[] == false
↓
false → 0
↓
[] → "" → 0
↓
0 == 0
↓
trueThe important lesson is not the individual conversions.
It’s:
==can trigger a chain of implicit conversions, includingToPrimitive, which makes seemingly unrelated values comparable.
Interview Answer
“
[]is truthy, so![]becomesfalse. Loose equality then converts the boolean to0, and the empty array is converted throughToPrimitiveto"", which converts to0. The comparison becomes0 == 0, so the result istrue. 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
NaNEverything 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;
// 1Nullish coalescing is more precise:
const page = input ?? 1;Now:
0 ?? 1;
// 0because ?? only falls back for:
null
undefinedInterview 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 != nullfor nullish presence,Number.isFinite()for numeric validity, andMap.has()when checking whether a key exists. I use??rather than||when onlynullandundefinedshould 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 == nullcan intentionally mean:
null OR undefinedThis 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";
// trueA 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 intentionallyFor 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.idbecause 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 informationSenior-Level Signal
A weak candidate memorizes:
[] == ![];
// trueA strong candidate explains why.
An even stronger candidate explains when the behavior matters architecturally:
“
typeofis 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 asToPrimitive. In production, I normalize external values at boundaries and keep domain comparisons explicit. Truthiness is also not an existence check, so APIs such asMap.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.