Senior 3 min · March 17, 2026

JavaScript Type Coercion — Why '149.99' + Tax Broke It

String '149.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • JavaScript has 8 data types: 7 primitives (string, number, boolean, null, undefined, Symbol, BigInt) and 1 object type (arrays, functions, objects).
  • Use typeof to check type – but watch for quirks: typeof null returns 'object' (a historic bug).
  • typeof function returns 'function', but functions are still objects.
  • Type coercion (implicit conversion) is the #1 source of bugs – always use ===.
  • null and undefined both mean 'no value' – use === to distinguish them.

JavaScript's type system is one of the most misunderstood parts of the language, and for good reason — it has genuine quirks that exist for historical reasons. Type coercion, the null/undefined split, and typeof's inconsistencies have tripped up millions of developers.

The good news is that once you understand the system on its own terms, it makes sense. This guide covers all 8 types, how typeof works, what coercion does, and how to write code that does not fall into the common traps.

The 8 JavaScript Types

JavaScript groups data into two camps: primitives (immutable, passed by value) and objects (mutable, passed by reference). There are 7 primitive types: string, number, boolean, null, undefined, Symbol, BigInt. Everything else is an object — including arrays, functions, dates, and plain objects. This split is the foundation of how JavaScript behaves.

JAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Primitives — immutable values
const str     = 'TheCodeForge'          // string
const num     = 42                       // number (no separate int/float)
const float   = 3.14                     // also number
const bool    = true                     // boolean
const nothing = null                     // null (intentional absence)
let  undef                               // undefined (not yet assigned)
const sym     = Symbol('id')             // Symbol (unique, no two are equal)
const big     = 9007199254740993n        // BigInt (integers beyond 2^53)

// Object — everything else
const obj  = { name: 'Alice', age: 30 } // plain object
const arr  = [1, 2, 3]                  // array (is an object)
const fn   = function() {}              // function (is an object)
const date = new Date()                 // Date (is an object)

console.log(typeof str)   // 'string'
console.log(typeof num)   // 'number'
console.log(typeof null)  // 'object'  ← historic bug, not a real object
console.log(typeof arr)   // 'object'
console.log(typeof fn)    // 'function' ← special case for functions
Primitives are immutable, but variables can be reassigned
When you do str = 'new', you're not changing the string — you're pointing the variable to a new string. The old string remains in memory until garbage collected.
Production Insight
Mixing primitives and objects in comparisons often leads to subtle bugs.
Always check if you're comparing values or references.
Use Object.is() for reliable equality (handles NaN and -0 correctly).
Key Takeaway
7 primitives, 1 object category.
All non-primitives are objects.
Functions are objects but typeof returns 'function'.
Is it a primitive or an object?
IfValue is one of: string, number, boolean, null, undefined, symbol, bigint
UseIt's a primitive — immutable, passed by value.
IfValue is anything else (array, function, date, plain object, etc.)
UseIt's an object — mutable, passed by reference.

null vs undefined

Both mean 'no value' but they mean it in different ways. undefined is the language's own 'not set yet'. null is your explicit signal that something was intentionally cleared. This distinction is crucial for debugging and for writing clean APIs.

JAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// undefined — the JS engine's default for 'not assigned'
let user;
console.log(user);             // undefined
console.log(user === undefined); // true

function greet(name) {
  console.log(name);           // undefined if called with no argument
}
greet();                       // undefined

// null — your code saying 'intentionally empty'
let currentUser = null;        // logged out — no user

// The null check gotcha
console.log(null == undefined);  // true  — loose equality
console.log(null === undefined); // false — strict equality (different types)

// Best practice: always use === and check explicitly
if (currentUser === null) {
  console.log('User logged out');
} else if (currentUser === undefined) {
  console.log('User state not initialised');
}
Avoid reassigning undefined
undefined is a global variable (in non-strict mode) that could theoretically be overwritten. Use null to represent intentional absence. In modern JS, you can also use void 0 for a safe undefined expression.
Production Insight
APIs that return null vs undefined often cause confusion in consumers.
Document your null contracts and validate inputs.
A common pattern: use ?? (nullish coalescing) to default only when null/undefined.
Key Takeaway
undefined = not yet set (JS default).
null = intentionally cleared (your choice).
Always use === to tell them apart.
When to use null vs undefined?
IfThe variable has never been assigned or the function was called without an argument
UseUse undefined — let JavaScript naturally set it.
IfYou explicitly want to clear a value (e.g., reset a cached result)
UseUse null — it's your signal, not the engine's.

Type Coercion — Where Bugs Hide

JavaScript automatically converts types in certain contexts. This is called implicit coercion. It is the source of most JS type bugs. The + operator is particularly dangerous: if either operand is a string, it concatenates. The - operator coerces both to numbers. Loose equality (==) also coerces, which is why it's almost always better to use strict equality (===).

JAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// + operator: if either operand is a string, concatenates
console.log(1 + '2')    // '12' — number coerced to string
console.log('3' - 1)    // 2    — string coerced to number
console.log(true + 1)   // 2    — true coerced to 1
console.log(false + 1)  // 1    — false coerced to 0

// == (loose) vs === (strict)
console.log(0 == false)  // true  — coercion
console.log(0 === false) // false — no coercion
console.log('' == false) // true  — both coerce to 0
console.log('' === false)// false

// The fix: always use === for comparisons
// Use explicit conversion when you mean to convert
const input = '42';  // string from a form input
const value = Number(input);  // explicit — clear intent
console.log(value + 1);  // 43, not '421'
Coercion Mental Model
  • + with a string → concatenation (string wins)
  • - , * , / → all operands coerced to numbers
  • == triggers coercion on both sides
  • === is the safe default — use it always
Production Insight
The most common coercion bug is string concatenation when you expected addition.
Always convert user input to numbers explicitly with Number() or parseInt().
Use ESLint rule 'eqeqeq' to ban == in your codebase.
Key Takeaway
Implicit coercion is the #1 JavaScript bug source.
Use === always.
Convert types explicitly with Number(), String(), Boolean().
How to avoid coercion bugs?
IfYou need to compare two values
UseAlways use === unless you have a very specific reason for ==.
IfYou need to convert a string to a number
UseUse Number(), parseInt(), or parseFloat() explicitly. Avoid unary +.
IfYou need to convert to boolean
UseUse Boolean() or !! (double NOT) — but prefer explicit Boolean() for clarity.

Checking Types Reliably

typeof works well for primitives (with the null exception). For objects, you need additional tools: Array.isArray() for arrays, instanceof for prototype chains, and Object.prototype.toString for a precise type tag.

JAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// For primitives: typeof works (except null)
function typeOf(val) {
  if (val === null)            return 'null';     // fix the null bug
  if (Array.isArray(val))     return 'array';    // typeof [] === 'object'
  return typeof val;
}

console.log(typeOf(null))      // 'null'
console.log(typeOf([1,2,3]))   // 'array'
console.log(typeOf('hello'))   // 'string'
console.log(typeOf(42))        // 'number'
console.log(typeOf({}))        // 'object'

// For objects: instanceof or Object.prototype.toString
console.log([] instanceof Array)    // true
console.log({} instanceof Object)   // true
console.log(Object.prototype.toString.call([]))  // [object Array]
Object.prototype.toString is the most reliable type check
Call as Object.prototype.toString.call(val) — returns '[object Type]' for any built-in type. Works for arrays, dates, regex, etc.
Production Insight
Using typeof for everything is a common trap that leads to false negatives (null, arrays).
Create a utility function like getType(val) that handles null and arrays.
In large codebases, consider using TypeScript for compile-time type safety.
Key Takeaway
typeof is great for primitives (except null).
Use Array.isArray() for arrays.
Object.prototype.toString is the universal fallback.
Which type check to use?
IfYou need to check for null
UseUse val === null
IfYou need to check for array
UseUse Array.isArray(val)
IfYou need to check the exact type of any value
UseUse Object.prototype.toString.call(val)

Symbol and BigInt — The Less Common Types

Symbol (ES6) creates unique, immutable identifiers. Two Symbols with the same description are never equal. BigInt (ES2020) lets you work with integers beyond Number.MAX_SAFE_INTEGER (2^53 - 1). They're less common but essential for advanced scenarios: Symbols for property keys that won't collide, BigInt for high-precision arithmetic like financial systems or 64-bit IDs.

JAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Symbol — guaranteed unique
const sym1 = Symbol('id');
const sym2 = Symbol('id');
console.log(sym1 === sym2); // false — even with same description

// Use case: metadata on objects without collision
const metadataKey = Symbol('metadata');
const user = { name: 'Alice', [metadataKey]: { created: '2025-01-01' } };
console.log(user[metadataKey]); // { created: '2025-01-01' }

// BigInt — large integers
const big1 = 9007199254740993n;          // n suffix
const big2 = BigInt('9007199254740993'); // from string
const sum = big1 + 1n;                  // 9007199254740994n
// Mixing BigInt and Number throws TypeError
// BigInt('1') + 1  // TypeError: Cannot mix BigInt and other types

// BigInt is not strictly equal to Number
console.log(1n == 1);  // true  (loose, coercion allowed? Actually 1n == 1 is true)
console.log(1n === 1); // false (different types)
BigInt arithmetic is integer-only
BigInt does not support fractional values. Use Number for decimals, but be aware of precision limits.
Production Insight
Symbols are not serialized by JSON.stringify — lost on API calls.
BigInt cannot be serialized to JSON natively; you need a custom replacer.
If your system uses 64-bit database IDs, always use BigInt for arithmetic.
Key Takeaway
Symbol = unique, non-colliding property keys.
BigInt = large integers beyond Number limit.
Both have serialization quirks in JSON.
When to use Symbol or BigInt?
IfYou need a unique property key that cannot collide with other keys
UseUse Symbol. Common in libraries for metadata.
IfYou need to handle integers > 2^53 - 1 (e.g., database IDs, crypto)
UseUse BigInt. Be aware of serialization limitations.
● Production incidentPOST-MORTEMseverity: high

The $1000 Bug: A String That Looked Like a Number

Symptom
Order total showed '149.99' instead of 149.99, and when adding tax, the result was '149.990.08' (string concatenation).
Assumption
The developer assumed both values were numbers because they looked like numbers when logged.
Root cause
One of the values came from a form input as a string. The + operator with a string operand coerces the other operand to a string and concatenates.
Fix
Always parse user input explicitly with Number() or parseFloat() before arithmetic. Use === to avoid accidental coercion.
Key lesson
  • Never assume input types are what they appear to be.
  • Explicit conversion > implicit coercion every time.
  • Use Number.isNaN() to validate parsed results.
Production debug guideSymptom → Immediate action → Fix4 entries
Symptom · 01
Unexpected string concatenation (e.g., 1 + '2' = '12')
Fix
Check both operands with typeof. Use Number() to convert strings explicitly before addition.
Symptom · 02
null vs undefined confusion in conditionals
Fix
Log the value and typeof. Use strict equality (===) to differentiate.
Symptom · 03
typeof null returns 'object'
Fix
Always use val === null for null checks. typeof is unreliable for null.
Symptom · 04
Array.isArray() returns false for objects that behave like arrays
Fix
Use Object.prototype.toString.call(val) for a reliable type tag.
★ Quick Type Debugging Cheat SheetWhen you hit a type-related bug, run these commands first.
Value is NaN but typeof says 'number'
Immediate action
Check if you accidentally coerced an undefined or string to a number.
Commands
console.log('value:', a, 'typeof:', typeof a, 'isNaN:', Number.isNaN(a))
console.log('parsed:', Number(a)) // see what Number() does
Fix now
Use Number.isNaN() to reliably detect NaN. Never use global isNaN() because it coerces.
Empty array returns 'object' from typeof+
Immediate action
Check if the variable is actually an array using Array.isArray()
Commands
console.log('Is array?', Array.isArray(val))
console.log('Type tag:', Object.prototype.toString.call(val))
Fix now
Replace typeof val === 'object' with Array.isArray(val) for arrays.
Strict equality fails even though values look the same+
Immediate action
Check for invisible whitespace or NaN.
Commands
console.log('length:', a?.length) // check string length
console.log('char codes:', [...a].map(c => c.charCodeAt(0)))
Fix now
Trim and use === after explicit conversion.
Type Comparison Table
TypeCategorytypeofMutable?Pass by
stringprimitive'string'NoValue
numberprimitive'number'NoValue
booleanprimitive'boolean'NoValue
nullprimitive'object'NoValue
undefinedprimitive'undefined'NoValue
Symbolprimitive'symbol'NoValue
BigIntprimitive'bigint'NoValue
Object (incl. arrays, functions)object'object' or 'function'YesReference

Key takeaways

1
JavaScript has 8 types
7 primitives (string, number, boolean, null, undefined, Symbol, BigInt) and objects.
2
typeof null === 'object' is a historic bug
always use val === null to check for null.
3
undefined means 'not yet set'; null means 'intentionally empty'. Use === to distinguish them.
4
Always use === instead of ==
loose equality triggers coercion in confusing ways.
5
Use Array.isArray() to check for arrays
typeof returns 'object' for them.
6
Symbols are unique identifiers; BigInt handles large integers beyond 2^53 - 1.
7
Explicit type conversion (Number(), String(), Boolean()) is safer than implicit coercion.

Common mistakes to avoid

4 patterns
×

Using == for comparison

Symptom
Loose equality coerces types, leading to unexpected results like 0 == false returning true. This causes bugs in conditionals, e.g., if (input == 0) may accidentally match falsy strings.
Fix
Always use === for equality checks. For null checks, use val === null or val == null if you want to catch both null and undefined (but be explicit with ===).
×

Forgetting that typeof null === 'object'

Symptom
A null check using typeof val === 'object' returns true, leading to false positives. Developers often think the value is an object when it's actually null.
Fix
Always check for null explicitly with val === null before using typeof. Create a helper function if you need robust type checking.
×

Assuming array is an object type without checking

Symptom
typeof [] returns 'object', so if (typeof arr === 'object') is true for arrays. But you may have intended to exclude arrays. This leads to incorrect branching.
Fix
Use Array.isArray(arr) to specifically check for arrays. If you need a generic 'object' check, combine: val !== null && typeof val === 'object' && !Array.isArray(val).
×

Mixing Number and BigInt in arithmetic

Symptom
JavaScript throws a TypeError: Cannot mix BigInt and other types. This often happens when fetching a number from an API and trying to add a BigInt.
Fix
Convert both to the same type before operations. Either use Number() for small numbers or keep everything as BigInt with explicit conversion: BigInt(numberValue).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What are the 8 data types in JavaScript?
Q02SENIOR
Why does typeof null return 'object' and how do you correctly check for ...
Q03JUNIOR
What is the difference between == and === in JavaScript?
Q04SENIOR
Explain the difference between null and undefined. When would you use ea...
Q05SENIOR
What is BigInt and when should you use it?
Q06SENIOR
How do you reliably check the type of a value in JavaScript?
Q01 of 06JUNIOR

What are the 8 data types in JavaScript?

ANSWER
The 8 types are: 7 primitives — string, number, boolean, null, undefined, Symbol, BigInt; and 1 object type (which includes arrays, functions, dates, and plain objects). Primitives are immutable and passed by value; objects are mutable and passed by reference.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why does typeof null return 'object'?
02
What is the difference between Number and number in JavaScript?
03
When should I use BigInt?
04
What is the difference between Symbol and Symbol.for()?
05
How do I check if a value is NaN?
🔥

That's JS Basics. Mark it forged?

3 min read · try the examples if you haven't

Previous
Variables in JavaScript — var let const
3 / 16 · JS Basics
Next
Operators in JavaScript