JavaScript Primitive Types: Complete Guide to All 7 Primitives (2026)
π― Interview-Ready Deep Dive β Every value in JavaScript falls into one of two camps: primitives or objects. Most developers can name a few primitives, but fewer understand why typeof null returns object, how strings can have methods without being objects, or when BigInt actually matters. These details separate surface-level knowledge from interview-ready fluency
JavaScript Primitive Types: Complete Guide to All 7 Primitives (2026)
Every value in JavaScript falls into one of two camps: primitives or objects. Most developers can name a few primitives, but fewer understand why typeof null returns "object", how strings can have methods without being objects, or when BigInt actually matters. These details separate surface-level knowledge from interview-ready fluency.
TL;DR: JavaScript has 7 primitive types:
string,number,bigint,boolean,undefined,symbol, andnull. Each is immutable, stored by value, and atomic. Theyβre the building blocks of all data in JavaScript β understand them, and the rest of the language makes more sense.
Quick Reference
| Type | typeof Result | Mutable? | JSON Support | Primary Use |
|---|---|---|---|---|
undefined | "undefined" | No | No | System-level absence |
null | "object" (bug) | No | Yes | Intentional empty value |
boolean | "boolean" | No | Yes | Logical true/false |
number | "number" | No | Yes | IEEE-754 floating-point |
bigint | "bigint" | No | No | Arbitrary precision integers |
string | "string" | No | Yes | UTF-16 text sequences |
symbol | "symbol" | No | No | Unique identifiers |
Table of Contents
- What Is a Primitive?
- The Seven Primitives Explained
- Equality at a Glance
- Temporary Wrapper Objects
- Internal Slots in Primitives
- Why JSON Doesnβt Support undefined
- Reliable Type Detection
- Common Pitfalls and Best Practices
- Interview Q&A
- Interview Self-Check
What Is a Primitive?
A primitive is an immutable, atomic value that is not an object and has no methods of its own.
According to the ECMAScript specification, primitives have these defining characteristics:
flowchart TD
PRIM["Primitive Value"] --> IMMUT["β
Immutable"]
PRIM --> BYVAL["β
Stored by Value"]
PRIM --> ATOM["β
Atomic"]
PRIM --> NOIDENT["β
No Identity"]
IMMUT --> IMMUT_DESC["Cannot be changed after creation"]
BYVAL --> BYVAL_DESC["Copied on assignment"]
ATOM --> ATOM_DESC["Not composed of sub-values"]
NOIDENT --> NOIDENT_DESC["Same value = indistinguishable"]
style PRIM fill:#60a5fa,stroke:#1e40af,color:#000
style IMMUT fill:#4ade80,stroke:#166534,color:#000
style BYVAL fill:#4ade80,stroke:#166534,color:#000
style ATOM fill:#4ade80,stroke:#166534,color:#000
style NOIDENT fill:#4ade80,stroke:#166534,color:#000
Why Understanding Primitives Matters
- Memory behavior: Primitives use value semantics (copied, not referenced)
- Equality comparisons: Two primitives with the same value are always equal
- Type coercion: Primitives follow specific conversion rules
- Performance: Primitives are generally faster to work with
- Interview frequency: Questions about primitives appear in 80%+ of JavaScript interviews
The Seven Primitives Explained
1. undefined β System-Level Absence
undefined represents βno value assignedβ. Only the JavaScript engine produces undefined automatically.
// Declared but unassigned variables
let x;
console.log(x); // undefined
// Missing object properties
let obj = {};
console.log(obj.missing); // undefined
// Missing function arguments
function test(a) {
console.log(a); // undefined if not provided
}
test();
// Functions without return statements
function noReturn() {}
console.log(noReturn()); // undefined
Key Distinction:
undefined= value not assigned (system) |null= value intentionally cleared (developer)
2. null β Intentional Absence
null represents explicit, intentional βno valueβ. Use it to indicate something is expected later or deliberately empty.
let user = null; // intentionally empty, will be assigned later
Why Does typeof null === "object"?
This is a legacy bug from 1995 that cannot be fixed due to web compatibility.
What Are Tagged Pointers?
In the original JavaScript engine (written in C), values were stored as 32-bit units. To save memory, the engine used a technique called tagged pointers β the lowest 1-3 bits of each value acted as a βtype tagβ that identified what kind of data it was:
| Type Tag (binary) | Value Type |
|---|---|
000 | Object |
1 | 31-bit signed integer |
010 | Double (floating-point) |
100 | String |
110 | Boolean |
The Bug:
null was represented as: 0x00000000 (all zeros)
β
Lowest 3 bits: 000 β matches "Object" tag!
The typeof operator checked the type tag, saw 000, and returned "object".
flowchart LR
subgraph Memory["32-bit Value"]
NULL["null = 0x00000000"]
BITS["...00000000 000"]
end
NULL --> BITS
BITS --> CHECK["typeof checks lowest bits"]
CHECK --> TAG["Sees 000 = Object tag"]
TAG --> BUG["Returns 'object'"]
style BUG fill:#f87171,stroke:#b91c1c,color:#000
Why Canβt It Be Fixed?
A fix was proposed for ECMAScript but rejected β too much existing code depends on this behavior:
// Millions of sites use this pattern
if (typeof value === "object" && value !== null) {
// handle objects
}
Changing typeof null to return "null" would break this code.
typeof null === "object"; // true β forever a historical artifact
How to Reliably Check for null
// β
Correct way
value === null
// β Don't use typeof (it returns "object")
typeof value === "null" // This doesn't work!
3. boolean β Logical True/False
The Boolean type has exactly two values: true and false.
Boolean(""); // false
Boolean("0"); // true (non-empty string)
Boolean([]); // true (empty array is truthy!)
Boolean({}); // true (empty object is truthy!)
Boolean(0); // false
Boolean(null); // false
Boolean(undefined); // false
The Falsy Values in JavaScript
flowchart TD
FALSY["Falsy Values"] --> F1["false"]
FALSY --> F2["0, -0, 0n"]
FALSY --> F3["'' (empty string)"]
FALSY --> F4["null"]
FALSY --> F5["undefined"]
FALSY --> F6["NaN"]
TRUTHY["Everything Else"] --> T1["'0' (string zero)"]
TRUTHY --> T2["[] (empty array)"]
TRUTHY --> T3["{} (empty object)"]
TRUTHY --> T4["'false' (string)"]
style FALSY fill:#f87171,stroke:#b91c1c,color:#000
style TRUTHY fill:#4ade80,stroke:#166534,color:#000
β οΈ Warning: Avoid Boolean wrapper objects β theyβre always truthy!
// β Dangerous
if (new Boolean(false)) {
console.log("This runs!"); // Because it's an object
}
// β
Correct
if (false) {
// Doesn't run
}
4. number β IEEE-754 Double Precision
JavaScript numbers are 64-bit floating-point values following the IEEE-754 standard. This single type handles integers, decimals, and special values.
// Safe integers: up to Β±(2β΅Β³ β 1)
const safeMax = Number.MAX_SAFE_INTEGER; // 9007199254740991
const safeMin = Number.MIN_SAFE_INTEGER; // -9007199254740991
// Special values
const inf = Infinity;
const negInf = -Infinity;
const notANumber = NaN;
const negZero = -0;
Why Does 0.1 + 0.2 !== 0.3? (Binary Floating-Point Explained)
0.1 + 0.2 === 0.3; // false!
0.1 + 0.2; // 0.30000000000000004
This isnβt a JavaScript bug β itβs how all IEEE-754 languages work (C, Java, Python, etc.).
The Problem: Binary Canβt Represent 0.1 Exactly
Just like 1/3 = 0.333β¦ repeats forever in decimal, 0.1 repeats forever in binary:
Decimal 0.1 in binary:
0.0001100110011001100110011001100110011001100110011001100110011...
β____________β (repeating pattern)
Since computers have finite bits (64 bits for JavaScript numbers), the value must be truncated:
// What JavaScript actually stores:
0.1 β 0.1000000000000000055511151231257827021181583404541015625
0.2 β 0.200000000000000011102230246251565404236316680908203125
// Their sum:
0.1 + 0.2 β 0.3000000000000000444089209850062616169452667236328125
The result is very close to 0.3, but not exactly 0.3.
Solutions:
// β
Option 1: Epsilon comparison
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
// β
Option 2: Work with integers (multiply by precision factor)
(0.1 * 10 + 0.2 * 10) / 10 === 0.3; // true
// β
Option 3: Round to fixed precision
Number((0.1 + 0.2).toFixed(10)) === 0.3; // true
Note:
Number.EPSILONworks well for values near 1. For very large or very small magnitudes, scale-relative tolerances are safer.
Why Is NaN === NaN False? (IEEE-754 Design Decision)
NaN === NaN; // false β this surprises most developers
Wait, isnβt NaN a primitive? Yes!
typeof NaN === "number"β NaN is a primitive number value, not an object. Yet itβs the only value in JavaScript that doesnβt equal itself. This exception is defined by the IEEE-754 floating-point specification, not JavaScriptβs normal equality rules.
This behavior is intentional, defined in the IEEE-754 floating-point specification. Hereβs why:
1. NaN Represents βUndefined Resultβ, Not a Single Value
NaN can arise from many different operations:
0 / 0; // NaN β indeterminate form
Math.sqrt(-1); // NaN β imaginary number
parseInt("abc"); // NaN β failed parsing
Infinity - Infinity; // NaN β indeterminate
Each NaN represents a different failure. Making them equal would falsely suggest they came from the same computation.
2. Self-Comparison Is a Key Detection Mechanism
The IEEE committee designed NaN so you can detect it with a simple check:
function isNaN_old(x) {
return x !== x; // Only NaN is not equal to itself
}
How to Properly Check for NaN:
// β Don't use direct comparison
NaN === NaN; // false
// β
Option 1: Number.isNaN() β recommended
Number.isNaN(NaN); // true
Number.isNaN("hello"); // false (doesn't coerce)
// β
Option 2: Object.is()
Object.is(NaN, NaN); // true
// β οΈ Avoid global isNaN() β it coerces first
isNaN("hello"); // true (coerces "hello" β NaN, then checks)
Negative Zero (-0)
JavaScript has both +0 and -0, but === treats them as equal:
0 === -0; // true
Object.is(0, -0); // false β they ARE different values
1 / -0; // -Infinity (different from 1/0!)
π Deep Dive: See JavaScript Equality Algorithms for why
-0exists and when the distinction matters.
Equality at a Glance
JavaScript has three primary equality algorithms β each handles NaN and -0 differently:
| Algorithm | NaN === NaN | 0 === -0 | Primary Use |
|---|---|---|---|
Strict Equality (===) | false | true | ===, switch, indexOf |
| SameValue | true | false | Object.is() |
| SameValueZero | true | true | Map, Set, includes() |
(These names come directly from the ECMAScript specification.)
// Quick examples
NaN === NaN; // false
Object.is(NaN, NaN); // true
[NaN].includes(NaN); // true
[NaN].indexOf(NaN); // -1
Object.is(0, -0); // false
0 === -0; // true
π Deep Dive: See JavaScript Equality Algorithms for spec-level details on Strict Equality, SameValue, and SameValueZero.
π Coercion: See == vs === and Type Coercion for how
==uses coercion before comparison and when to use each operator.
5. bigint β Arbitrary Precision Integers
Introduced in ES2020, BigInt handles integers beyond Numberβs safe range.
const id = 9007199254740993n;
const huge = BigInt("999999999999999999999");
Key Rules
// β Cannot mix BigInt and Number
10n + 5; // TypeError
// β
Explicit conversion required
10n + BigInt(5); // 15n
Number(10n) + 5; // 15
When to Use BigInt
| Use Case | Why BigInt? |
|---|---|
| Cryptography | Requires exact large integer operations |
| Financial calculations | Precision-critical arithmetic |
| Large identifiers | Database IDs, timestamps beyond safe range |
| Scientific computing | Arbitrary precision requirements |
β οΈ
BigIntis not supported by JSON. You must convert to string before serialization.
6. string β Immutable UTF-16 Sequences
Strings in JavaScript are immutable, meaning they cannot be changed after creation.
let s = "hello";
s[0] = "H"; // Silently ignored
console.log(s); // "hello" (unchanged)
// Concatenation creates a NEW string
let newStr = s + " world";
console.log(s); // "hello" (still unchanged)
UTF-16 and Emoji Length
Some characters occupy two UTF-16 code units (surrogate pairs):
"π".length; // 2 (not 1!)
"hello".length; // 5
// To count actual characters
[..."π"].length; // 1 (spread handles surrogates)
Why String Indexing Works
JavaScript temporarily wraps strings in String objects (autoboxing):
"hello"[1]; // "e" β works via temporary wrapper
7. symbol β Unique Identity Values
A Symbol is a unique, immutable primitive commonly used for object property keys.
const role = Symbol("role");
const user = { [role]: "admin" };
console.log(user[role]); // "admin"
console.log(user.role); // undefined (not accessible via string)
Local vs Global Symbols
// Local symbols β always unique
Symbol("id") === Symbol("id"); // false
// Global symbol registry β shared
Symbol.for("id") === Symbol.for("id"); // true
Why Symbols Exist
flowchart TD
SYM["Symbol Use Cases"] --> UC1["π Hidden object properties"]
SYM --> UC2["π« Avoid key collisions"]
SYM --> UC3["π§ Well-known protocols"]
SYM --> UC4["π¦ Library-safe extensions"]
UC3 --> WK1["Symbol.iterator"]
UC3 --> WK2["Symbol.toStringTag"]
UC3 --> WK3["Symbol.hasInstance"]
style SYM fill:#60a5fa,stroke:#1e40af,color:#000
β οΈ Symbols cannot be auto-converted to strings (prevents accidental leakage):
"hello" + Symbol(); // TypeError
String(Symbol("x")); // "Symbol(x)" β explicit only
Temporary Wrapper Objects: How Primitives Have Methods
When you call a method on a primitive, JavaScript performs autoboxing:
"hello".toUpperCase(); // "HELLO"
Internally:
flowchart LR
A["'hello'.toUpperCase()"] --> B["new String('hello')"]
B --> C[".toUpperCase()"]
C --> D["'HELLO'"]
D --> E["Discard wrapper"]
style B fill:#fbbf24,stroke:#b45309,color:#000
style E fill:#f87171,stroke:#b91c1c,color:#000
- JavaScript creates a temporary wrapper object (
new String("hello")) - Calls the method on the wrapper
- Returns the result
- Discards the wrapper immediately
The primitive value itself remains unchanged.
Internal Slots in Primitives
Some primitives carry intrinsic internal slots (not object properties):
| Primitive | Internal Slot | Purpose |
|---|---|---|
| String | [[StringData]] | UTF-16 sequence storage |
| BigInt | [[BigIntData]] | Arbitrary-length number |
| Symbol | [[Description]] | Debug label (optional) |
These are specification-level details that explain how primitives store their data internally.
Why JSON Doesnβt Support undefined
JSON (defined by RFC 8259) only supports: object, array, string, number, boolean, and null.
Why no undefined?
- JavaScript-specific: Most languages donβt have an equivalent
- Data format, not runtime:
undefinedarises from runtime behavior - Single empty value: JSON uses
nullto avoid duplication
JSON.stringify({ a: undefined }); // "{}" β key disappears
JSON.stringify([undefined]); // "[null]" β becomes null
JSON.stringify({ a: null }); // '{"a":null}' β preserved
Reliable Type Detection
Using typeof
typeof "hello"; // "string"
typeof 42; // "number"
typeof 42n; // "bigint"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol(); // "symbol"
typeof null; // "object" β watch out!
Comprehensive Primitive Check
function isPrimitive(value) {
return (
value === null ||
(typeof value !== "object" && typeof value !== "function")
);
}
// More explicit version
function getPrimitiveType(value) {
if (value === null) return "null";
const type = typeof value;
if (["string", "number", "bigint", "boolean", "undefined", "symbol"].includes(type)) {
return type;
}
return null; // Not a primitive
}
Common Pitfalls and Best Practices
β Pitfall 1: Using == Instead of ===
// Loose equality with coercion
0 == ""; // true (both coerce to 0)
0 == "0"; // true
null == undefined; // true
// β
Use strict equality
0 === ""; // false
0 === "0"; // false
null === undefined; // false
β Pitfall 2: Expecting String Mutation
let str = "hello";
str.toUpperCase();
console.log(str); // "hello" β unchanged!
// β
Assign the result
str = str.toUpperCase();
console.log(str); // "HELLO"
β Pitfall 3: Floating-Point Comparisons
// β Direct comparison fails
0.1 + 0.2 === 0.3; // false
// β
Use epsilon comparison
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
// β
Or use integers (cents instead of dollars)
10 + 20 === 30; // true
β Pitfall 4: Using Primitive Wrapper Constructors
// β Creates an object, not a primitive
let bool = new Boolean(false);
if (bool) console.log("Runs!"); // Objects are truthy
// β
Use literals
let bool = false;
if (bool) console.log("Doesn't run");
Interview Q&A
Q: What are the 7 primitive types in JavaScript?
The seven primitive types are:
stringβ Immutable text sequencesnumberβ IEEE-754 64-bit floating-pointbigintβ Arbitrary precision integersbooleanβ Logical true/falseundefinedβ System-level absencesymbolβ Unique identifiersnullβ Intentional absence
Each is immutable, stored by value, and atomic.
Q: Why is null considered a primitive when typeof null === "object"?
The typeof null === "object" is a historical bug from 1995. In the original JavaScript implementation:
- Objects were tagged with a binary suffix
000 nullwas represented as all zero bits (0x00000000)- The runtime misclassified it because it also ends in
000
This cannot be fixed due to backward compatibility with billions of websites. Despite this quirk, null is definitively a primitive according to the ECMAScript specification.
Q: What is the difference between undefined and null?
| Aspect | undefined | null |
|---|---|---|
| Origin | System-produced | Developer-assigned |
| Meaning | No value assigned | Intentionally empty |
| typeof | "undefined" | "object" (bug) |
| JSON | Not supported | Supported |
| Use case | Uninitialized variables, missing properties | Explicitly cleared values |
let x; // undefined (system)
let y = null; // null (intentional)
Q: How do primitives have methods if they're not objects?
JavaScript uses autoboxing (temporary wrapper objects):
- When you call a method on a primitive (e.g.,
"hello".toUpperCase()) - JavaScript creates a temporary wrapper object (
new String("hello")) - The method is called on this wrapper
- The result is returned
- The wrapper is immediately discarded
The primitive itself never changes β wrapper objects are temporary and invisible.
Q: Why does 0.1 + 0.2 !== 0.3?
JavaScript uses IEEE-754 double-precision floating-point for numbers. The values 0.1 and 0.2 cannot be represented exactly in binary:
0.1becomes0.10000000000000000555111512312578270211815834045410156250.2becomes0.200000000000000011102230246251565404236316680908203125- Their sum is
0.30000000000000004
Solutions:
- Use epsilon comparison:
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON - Use integers (cents instead of dollars)
- Use
BigIntfor precision-critical calculations
Q: When should you use BigInt instead of number?
Use BigInt when:
- Working with integers larger than
Number.MAX_SAFE_INTEGER(2β΅Β³ β 1) - Performing cryptographic operations
- Handling financial calculations requiring exact precision
- Working with large database IDs or timestamps
- Any scenario where precision loss is unacceptable
Remember: BigInt cannot mix with number without explicit conversion, and JSON doesnβt support it.
Q: What problem does Symbol solve?
Symbols solve property key collision problems:
- Unique keys: Each
Symbol()is guaranteed unique - Hidden properties: Symbols donβt appear in
for...inorObject.keys() - Library safety: Third-party code canβt accidentally overwrite symbol properties
- Protocols: Well-known symbols (
Symbol.iterator,Symbol.toStringTag) define language behavior
const id = Symbol("id");
const obj = { [id]: "secret" };
Object.keys(obj); // [] β symbol not visible
Q: Why doesn't JSON support undefined?
JSON is defined by RFC 8259 as a language-independent data format. Three reasons undefined is excluded:
- JavaScript-specific: Most languages donβt have
undefined - Runtime concept:
undefinedrepresents runtime state (missing values) - Single empty value: JSON uses
nullas the only βemptyβ value
When serializing:
- Object keys with
undefinedvalues disappear - Array elements with
undefinedbecomenull
Q: How do you reliably check for NaN?
// β Don't use direct comparison
NaN === NaN; // false (NaN is not equal to itself)
// β
Option 1: Number.isNaN() β recommended
Number.isNaN(NaN); // true
Number.isNaN("hello"); // false (doesn't coerce)
// β
Option 2: Object.is()
Object.is(NaN, NaN); // true
// β οΈ Avoid global isNaN() β it coerces first
isNaN("hello"); // true (coerces to NaN first)
Q: Why is .length not always the character count for strings?
JavaScript strings are UTF-16 encoded. Some characters (like emojis) require two code units (surrogate pairs):
"hello".length; // 5 β each letter is one code unit
"π".length; // 2 β emoji uses two code units
"π¨βπ©βπ§".length; // 8 β family emoji with ZWJ sequences
// To count actual characters:
[..."π"].length; // 1 β spread handles surrogates
[...str].length; // Counts grapheme clusters (mostly)
For accurate character counting, use Intl.Segmenter or a library like grapheme-splitter.
Interview Self-Check
Test your understanding β try answering each question before revealing the answer.
1. What makes a primitive different from an object?
Reveal Answer
Primitives are:
- Immutable: Cannot be changed after creation
- Stored by value: Assignments copy the actual value
- Atomic: Not composed of sub-values
- No identity: Two primitives with the same value are indistinguishable
- No prototypes: Methods come from temporary wrapper objects
Objects are mutable, stored by reference, and have unique identity.
2. Why does new Boolean(false) behave differently than false?
Reveal Answer
new Boolean(false) creates an object, not a primitive. In JavaScript, all objects are truthy β even if they wrap a falsy value.
if (new Boolean(false)) {
console.log("Runs!"); // Because objects are truthy
}
if (false) {
console.log("Doesn't run"); // Primitive false is falsy
}
This is why you should never use primitive wrapper constructors.
3. What happens when you try to assign a property to a primitive?
Reveal Answer
In non-strict mode, JavaScript:
- Creates a temporary wrapper object
- Assigns the property to the wrapper
- Discards the wrapper immediately
The property is lost, and the primitive remains unchanged:
let str = "hello";
str.customProp = "value"; // Assigned to temporary wrapper
console.log(str.customProp); // undefined β wrapper was discarded
In strict mode, this throws a TypeError.
4. How do you check if a value is a primitive?
Reveal Answer
function isPrimitive(value) {
return (
value === null ||
(typeof value !== "object" && typeof value !== "function")
);
}
// Tests
isPrimitive("hello"); // true
isPrimitive(42); // true
isPrimitive(null); // true
isPrimitive(undefined); // true
isPrimitive({}); // false
isPrimitive([]); // false
5. What is the difference between Symbol() and Symbol.for()?
Reveal Answer
| Method | Scope | Uniqueness |
|---|---|---|
Symbol() | Local | Always creates a new unique symbol |
Symbol.for() | Global registry | Returns existing symbol if key exists |
// Local symbols
Symbol("id") === Symbol("id"); // false β different symbols
// Global symbols (shared via registry)
Symbol.for("id") === Symbol.for("id"); // true β same symbol
Symbol.keyFor(Symbol.for("id")); // "id" β can retrieve key
Use Symbol.for() when you need the same symbol across different parts of your application or across realms.
Summary Checklist
- Can identify all 7 primitive types
- Understands primitives are immutable
- Knows the
typeof null === "object"quirk and why it exists - Can distinguish
nullvsundefinedsemantically - Understands number precision limitations (IEEE-754)
- Knows when to use
BigIntand its limitations - Can explain how
Symbolprovides unique identifiers - Understands autoboxing (temporary wrapper objects)
- Knows why JSON doesnβt support
undefined - Can write reliable type detection functions
- Avoids primitive wrapper constructors
- Prefers strict equality (
===) for comparisons
Final Takeaway
JavaScript primitives are simple by design but deep in implications. They define:
- How memory works (value semantics)
- How equality behaves (by value comparison)
- How coercion operates (type conversion rules)
- How JSON serialization works (supported vs unsupported types)
Mastering primitives gives you:
- β Predictable code
- β Fewer bugs
- β Strong interview confidence
This is not trivia β it is core language literacy.
Further Reading
This Series:
- JavaScript Equality Algorithms β ===, SameValue, and SameValueZero explained
- == vs === and Type Coercion β Abstract Equality and coercion rules
External Resources: