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.
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 functionsPrimitives 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'.
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.
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.
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 (===).
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().
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.
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.
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.
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.
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.