Data Types & Coercion Interview Questions & Answers
33 questions Updated 2026-06-17
JavaScript interview questions on primitive types, type coercion, == vs ===, truthy/falsy values and checking types, with examples.
Read the in-depth guideJavaScript Data Types & Type Coercion — The Complete GuideThere are seven primitive types: string, number, boolean, null,
undefined, symbol, and bigint. Everything that isn't a primitive is an
object — and that includes arrays, functions, dates, and null's
misleading typeof.
typeof 'hi' // 'string'
typeof 42 // 'number'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof 10n // 'bigint'
typeof null // 'object' <- historical bug, null IS a primitive
Two defining traits: primitives are immutable (you can't change the value
itself, only rebind the variable) and they're compared by value, whereas
objects are mutable and compared by reference. When you call a method like
'hi'.toUpperCase(), JS temporarily wraps the primitive in an object
(autoboxing) and discards it afterward.
=== is strict equality: it compares value and type with no
conversion — if the types differ, it's immediately false. == is loose
equality: it coerces the operands toward a common type first, which
produces a list of surprising results.
0 === '' // false (number vs string)
0 == '' // true (both coerce to 0)
0 == '0' // true
'' == '0' // false <- not even transitive!
null == undefined // true (special-cased)
NaN == NaN // false
[] == ![] // true (classic "wtf" — [] coerces to '', ![] is false -> 0)
Prefer === virtually always — it's predictable and the coercion rules
behind == are a frequent bug source. The one common idiomatic exception is
x == null, which conveniently matches both null and undefined.
Coercion is JavaScript automatically converting a value from one type to
another. It comes in two flavors: implicit (the engine does it for you
during an operation, e.g. '5' * 2) and explicit (you ask for it,
Number('5'), String(5), Boolean(0)).
The operator that trips everyone up is +: if either side is a string, it
does string concatenation; otherwise it does numeric addition. The other
arithmetic operators (-, *, /) have no string meaning, so they coerce to
numbers.
1 + '2' // '12' (+ with a string -> concatenation)
1 - '2' // -1 (- forces numeric -> 1 - 2)
'5' * '2' // 10 (* forces numeric)
true + 1 // 2 (true -> 1)
[] + {} // '[object Object]' (both -> strings)
In an interview, narrate the rule ("+ prefers strings, the rest prefer
numbers") rather than memorizing every edge case.
There are exactly eight falsy values — memorize the list, because everything else is truthy:
false
0
-0
0n // BigInt zero
'' // empty string
null
undefined
NaN
The famous gotchas are the things people expect to be falsy but aren't:
'0', 'false', [] (empty array), and {} (empty object) are all
truthy.
if ([]) console.log('runs!') // empty array is truthy
if ('0') console.log('runs!') // non-empty string is truthy
Boolean([]) // true
This matters for if conditions, ||/&&, and the nullish coalescing
operator ?? (which, unlike ||, only falls back on null/undefined, not
on 0 or '').
Both represent "no value," but they signal different intents:
undefinedis the language's default "absence" — a declared-but-unassigned variable, a missing object property, a missing function argument, or the return of a function with noreturn. The engine produces it.nullis an intentional, explicit "no value here" that you assign to say "this is deliberately empty."
let a // undefined (never assigned)
const obj = {}
obj.missing // undefined (no such property)
const b = null // null (you chose emptiness)
typeof undefined // 'undefined'
typeof null // 'object' <- long-standing bug, never fixed for compat
null == undefined // true (loose — they're "equal absences")
null === undefined // false (different types)
Use Array.isArray(value). You can't use typeof, because arrays are
objects, so typeof [] returns 'object' — indistinguishable from a plain
object or null.
typeof [] // 'object' <- useless for arrays
Array.isArray([]) // true
Array.isArray({}) // false
Array.isArray('abc') // false
Array.isArray is also more robust than the older
value instanceof Array trick, because instanceof breaks across
realms (e.g. an array coming from an <iframe> has a different Array
constructor, so instanceof returns false). Array.isArray works
regardless of realm.
NaN ("Not-a-Number") is, by the IEEE-754 spec and ECMAScript, the only
value not equal to itself — there are many distinct computations that produce
"not a number," so the standard defines all comparisons with NaN (including
NaN === NaN) as false. You therefore can't detect it with ===.
NaN === NaN // false
0 / 0 // NaN
Number('abc') // NaN
Number.isNaN(NaN) // true reliable
Number.isNaN('abc') // false (no coercion — 'abc' simply isn't NaN)
isNaN('abc') // true misleading: it coerces 'abc' -> NaN first
Use Number.isNaN(value) (ES2015), which checks "is this literally the
NaN value." Avoid the global isNaN, which coerces its argument to a number
first and so reports true for plenty of non-NaN inputs. (Another trick that
works: value !== value is true only for NaN.)
Object.is(a, b) is "same-value" equality. It behaves like === except for
two edge cases: it treats NaN as equal to itself, and it distinguishes +0
from -0.
Object.is(NaN, NaN) // true (=== gives false)
Object.is(0, -0) // false (=== gives true)
Object.is(1, 1) // true
It's the same algorithm React uses to decide whether state/props changed. Use
=== for normal comparisons; reach for Object.is when those two NaN/-0
edge cases actually matter.
JavaScript has a signed zero: +0 and -0 are distinct bit patterns. They're
equal under == and ===, but distinguishable with Object.is or by
dividing (which exposes the sign via Infinity).
+0 === -0 // true
Object.is(+0, -0) // false
1 / +0 // Infinity
1 / -0 // -Infinity
-0 arises from operations like -1 * 0 or Math.round(-0.1). It rarely matters,
but can surprise you in sign-sensitive math or when used as a Map key.
typeof returns 'function' for any callable — function declarations,
expressions, arrows, classes, and methods. This is a special case: functions are
technically objects, but typeof singles them out.
typeof function () {} // 'function'
typeof (() => {}) // 'function'
typeof class {} // 'function' (classes are functions under the hood)
typeof Math.max // 'function'
typeof [] // 'object' (arrays are NOT singled out)
It's the reliable way to check "is this callable?" before invoking
(typeof cb === 'function').
typeof returns one of eight strings:
typeof 'a' // 'string'
typeof 1 // 'number'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof 10n // 'bigint'
typeof Symbol() // 'symbol'
typeof function(){}// 'function'
typeof {} // 'object' (also arrays, null, dates, etc.)
The famous quirk is typeof null === 'object' — a bug kept for backward
compatibility. Anything not in the first seven categories (including arrays and
null) reports 'object', which is why you need Array.isArray and
=== null.
Primitives have no methods, yet 'hi'.toUpperCase() works because JavaScript
temporarily wraps the primitive in its object form (String, Number,
Boolean), calls the method, then discards the wrapper.
'hi'.length // 2 — boxed to a String object momentarily
(5).toFixed(2) // '5.00' — boxed to Number
// explicit wrapper objects are an anti-pattern:
const n = new Number(5)
typeof n // 'object', not 'number'
n === 5 // false
Never use new Number/new String — the resulting objects break === and are
always truthy (new Boolean(false) is truthy!). Let autoboxing happen
implicitly.
Number(x)converts the entire string to a number; any invalid character makes the whole thingNaN. Handles decimals.parseInt(x, radix)reads an integer prefix, stopping at the first non-numeric character, and ignores the rest. Always pass the radix.
Number('42px') // NaN
parseInt('42px', 10)// 42 (parses the leading 42)
Number('3.14') // 3.14
parseInt('3.14', 10)// 3 (integer only)
parseInt('0x1F', 16)// 31
Number('') // 0 (parseInt('') is NaN)
Use Number/parseFloat for exact full-string conversion; parseInt for
extracting a leading integer (e.g. '20px' -> 20).
The abstract equality (==) algorithm, simplified:
- Same type -> compare like
===. null == undefined->true(and they equal nothing else).- number vs string -> convert the string to a number.
- boolean vs anything -> convert the boolean to a number (
true->1,false->0). - object vs primitive -> convert the object to a primitive (
valueOf/toString), then retry.
'5' == 5 // string->number: 5 == 5 -> true
true == 1 // boolean->number: 1 == 1 -> true
[] == 0 // [] -> '' -> 0; 0 == 0 -> true
[] == ![] // ![] is false->0; [] -> 0; true
Knowing these steps explains every "wat" example — and is the best argument for
always using ===.
When an object is used where a primitive is expected, JS calls its
Symbol.toPrimitive method (if present) with a hint ('number', 'string',
or 'default'); otherwise it falls back to valueOf then toString.
const money = {
amount: 100,
[Symbol.toPrimitive](hint) {
return hint === 'string' ? `$${this.amount}` : this.amount
},
}
`${money}` // '$100' (hint 'string')
money + 1 // 101 (hint 'default' -> number)
money * 2 // 200 (hint 'number')
This lets you control how objects behave in concatenation, arithmetic, and template literals.
Without Symbol.toPrimitive, object-to-primitive conversion uses these two: for a
number hint it tries valueOf first; for a string hint it tries
toString first. Whichever returns a primitive wins.
const obj = {
valueOf() { return 42 },
toString() { return 'hello' },
}
obj + 1 // 43 (default/number hint -> valueOf)
`${obj}` // 'hello' (string hint -> toString)
String(obj) // 'hello'
Number(obj) // 42
Override toString (and sometimes valueOf) to make your objects coerce
sensibly in logs and expressions.
Template literals coerce every interpolated value to a string (using
String() / the object's toString). This is convenient but can produce
[object Object] or undefined/null text.
`count: ${5}` // 'count: 5'
`arr: ${[1, 2, 3]}` // 'arr: 1,2,3' (array toString joins with commas)
`obj: ${ {a: 1} }` // 'obj: [object Object]'
`val: ${null}` // 'val: null'
For objects, interpolate a specific field or JSON.stringify(obj) rather than
relying on the default [object Object].
JSON.stringify silently drops or transforms several types: undefined,
functions, and symbols are omitted in objects (or become null in arrays);
NaN/Infinity become null; BigInt throws; and Date becomes an ISO
string.
JSON.stringify({ a: undefined, b: () => {}, c: NaN })
// '{"c":null}' — a and b dropped, NaN -> null
JSON.stringify([undefined, function(){}, 1])
// '[null,null,1]'
JSON.stringify(10n) // TypeError: BigInt not serializable
It also calls a value's toJSON() if present, and ignores non-enumerable
properties. Know these when serializing for APIs or storage.
All JS numbers are 64-bit IEEE-754 doubles. Decimal fractions like 0.1 can't
be represented exactly in binary, so they're stored as the nearest approximation
and rounding errors accumulate.
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
// compare with a tolerance instead:
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
For money, work in integer cents or use a decimal library. Never compare
floats with ===; use an epsilon tolerance.
BigInt represents integers of arbitrary precision, beyond the safe integer
limit of Number (2^53 - 1). Create one with an n suffix or BigInt().
Number.MAX_SAFE_INTEGER // 9007199254740991
9007199254740991 + 2 // 9007199254740992 (wrong! lost precision)
9007199254740991n + 2n // 9007199254740993n (exact)
Caveats: you can't mix BigInt and Number in arithmetic (1n + 1 throws),
typeof 1n === 'bigint', and it's integer-only (no decimals). Use it for large
IDs, timestamps in nanoseconds, and exact big-integer math.
a ?? b returns b only when a is null or undefined — unlike ||,
which falls back on any falsy value (0, '', false). This makes ??
correct for defaults where 0/'' are valid.
const count = 0
count || 10 // 10 treats valid 0 as "missing"
count ?? 10 // 0 only null/undefined trigger the default
const name = '' ?? 'anon' // '' (empty string is kept)
Use ?? when only "absent" should trigger the fallback. You can't mix ??
directly with &&/|| without parentheses (a syntax error by design).
?. short-circuits to undefined if the value before it is null/undefined,
instead of throwing — letting you safely access deep properties, call maybe-
missing methods, and index possibly-absent values.
user?.address?.city // undefined if user or address is null
user?.getName?.() // calls only if getName exists
arr?.[0] // safe index access
It pairs naturally with ??: user?.name ?? 'anon'. Note it stops at the first
nullish link and only guards against null/undefined — not other errors.
=== compares references, so two distinct arrays/objects with identical
contents are not equal. You must compare contents yourself.
[1, 2] === [1, 2] // false (different references)
// shallow array compare:
const eq = (a, b) => a.length === b.length && a.every((v, i) => v === b[i])
// quick (but flawed) deep compare:
JSON.stringify(a) === JSON.stringify(b) // order/undefined-sensitive
JSON.stringify works for simple data but breaks on key order, undefined,
functions, and cycles. For robust deep equality, use a library (lodash
isEqual).
Several, with different strictness:
Number('42') // 42 (whole string; '' -> 0, '4a' -> NaN)
parseInt('42px', 10) // 42 (leading integer)
parseFloat('3.14m') // 3.14 (leading float)
+'42' // 42 (unary plus — concise coercion)
'42' * 1 // 42 (arithmetic coercion)
Number()/unary + are strict (invalid -> NaN); parseInt/parseFloat are
lenient (parse a prefix). Always validate with Number.isNaN afterward when the
input is untrusted.
!!x coerces any value to its boolean equivalent. The first ! converts to
the inverted boolean, the second ! flips it back — giving the truthiness as a
real true/false.
!!'hello' // true
!!0 // false
!!null // false
!![] // true (empty array is truthy)
Boolean('hello') // true — equivalent, more explicit
It's a common idiom to normalize a value to a strict boolean (e.g. before
returning from a predicate or storing a flag). Boolean(x) does the same thing
more readably.
A Symbol is a unique, immutable primitive, mainly used as non-colliding
object keys. Every Symbol() is distinct, even with the same description.
const id = Symbol('id')
const obj = { [id]: 123 }
obj[id] // 123
Symbol('x') === Symbol('x') // false — always unique
Uses: private-ish keys (not enumerable in for...in/JSON.stringify), and
well-known symbols that customize behavior (Symbol.iterator,
Symbol.toPrimitive, Symbol.asyncIterator). Symbol.for('k') accesses a
shared global registry.
An array's toString joins its elements with commas. So an empty array becomes
'', a single-element array becomes that element's string, and + with a string
concatenates — leading to several classic surprises.
[] + [] // '' (both -> '')
[] + {} // '[object Object]'
[1, 2] + [3] // '1,23' ('1,2' + '3')
Number([]) // 0 ('' -> 0)
Number([5]) // 5 ('5' -> 5)
Number([1, 2]) // NaN ('1,2' -> NaN)
These underpin the [] == ![] brain-teasers. The rule: array -> string (comma
join) -> then number if needed.
NaN results from invalid or undefined mathematical operations — converting
non-numeric strings, 0/0, Infinity - Infinity, or arithmetic on undefined.
And NaN is contagious: any arithmetic with it yields NaN.
Number('abc') // NaN
0 / 0 // NaN
Math.sqrt(-1) // NaN
undefined + 1 // NaN
parseInt('xyz', 10) // NaN
NaN + 5 // NaN (propagates)
Because it spreads, a single bad value can turn a whole calculation into NaN —
guard inputs and check with Number.isNaN.
undefined is a value: a declared variable that hasn't been assigned, or a
missing property. "Not defined" means the identifier doesn't exist at all —
accessing it throws a ReferenceError.
let a
console.log(a) // undefined (declared, no value)
console.log(b) // ReferenceError: b is not defined
const obj = {}
console.log(obj.x) // undefined (missing property, no error)
So undefined is recoverable (it's just a value); a "not defined" reference is an
error. typeof is the safe way to check the latter without throwing.
typeof is special-cased to not throw a ReferenceError for an undeclared
identifier — it returns 'undefined'. This makes it the safe way to feature-
detect a global that may not exist.
typeof someUndeclaredVar // 'undefined' (no error)
someUndeclaredVar // ReferenceError
// safe environment checks:
if (typeof window !== 'undefined') { /* browser */ }
Caveat: this leniency does not apply to let/const in the temporal dead
zone — typeof x before a let x declaration still throws.
A primitive value itself can never be changed — operations that look like mutation actually create a new value. You can reassign the variable, but the original primitive is untouched.
let s = 'hello'
s.toUpperCase() // 'HELLO' (a new string)
console.log(s) // 'hello' (original unchanged)
s[0] = 'J' // silently fails (or throws in strict mode)
s = 'world' // reassigning the variable is fine
Contrast objects/arrays, which are mutable (you can change their contents in place). Immutability is why primitives compare by value and are safe to share.
Relational operators compare strings lexicographically by UTF-16 code unit, character by character. This is case-sensitive (uppercase letters have lower code points than lowercase) and not locale-aware.
'apple' < 'banana' // true
'Z' < 'a' // true (90 < 97)
'10' < '9' // true (string compare: '1' < '9')
'10' < 9 // false (mixed -> numeric: 10 < 9)
For human-friendly, locale-aware ordering (accents, case-insensitive), use
a.localeCompare(b). Note mixed string/number comparisons coerce to numbers.
Shallow equality compares the top level: same reference, or matching primitive values / first-level properties. Deep equality recursively compares nested structures for equivalent contents.
const a = { x: { y: 1 } }
const b = { x: { y: 1 } }
a.x === b.x // false (different nested references)
// shallow: equal keys but nested refs differ -> not equal
// deep: recursively equal -> equal
React uses shallow comparison for re-render decisions (props/state), which is why
mutating nested objects can be missed. Deep equality (lodash isEqual) is
costlier but compares full structure.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.