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 Guide

There 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:

  • undefined is 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 no return. The engine produces it.
  • null is 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 thing NaN. 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:

  1. Same type -> compare like ===.
  2. null == undefined -> true (and they equal nothing else).
  3. number vs string -> convert the string to a number.
  4. boolean vs anything -> convert the boolean to a number (true->1, false->0).
  5. 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.