Skip to content

Data Types & Coercion Interview Questions & Answers

33 questions Updated 2026-06-17 Share:

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(opens in new tab)
33 of 33

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.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel