JavaScript Data Types: Primitives and Objects

JavaScript is dynamically typed, meaning variables don't have fixed types—the values they hold do. Unlike statically-typed languages where you declare `int x = 5`, JavaScript lets you assign any...

Key Insights

  • JavaScript has seven primitive types (string, number, bigint, boolean, undefined, null, symbol) that are immutable and compared by value, while objects are mutable and compared by reference—this distinction fundamentally affects how you pass data and check equality.
  • Primitives automatically “borrow” methods from wrapper objects through auto-boxing, allowing "hello".toUpperCase() to work even though strings are primitives, but explicitly creating wrapper objects with new String() is an anti-pattern that causes bugs.
  • Type coercion happens implicitly in JavaScript ("5" + 1 returns "51"), so use strict equality (===) and explicit conversions (Number(), String()) to avoid unexpected behavior in production code.

Introduction to JavaScript’s Type System

JavaScript is dynamically typed, meaning variables don’t have fixed types—the values they hold do. Unlike statically-typed languages where you declare int x = 5, JavaScript lets you assign any value to any variable at any time. This flexibility comes with responsibility: you need to understand what types you’re working with.

The type system splits into two categories: primitives and objects. Primitives are simple, immutable values. Objects are complex, mutable collections of properties. This isn’t just academic—it affects equality comparisons, function arguments, memory usage, and debugging.

// Primitives
typeof 42;              // "number"
typeof "hello";         // "string"
typeof true;            // "boolean"
typeof undefined;       // "undefined"
typeof Symbol("id");    // "symbol"
typeof 123n;            // "bigint"

// Objects
typeof {};              // "object"
typeof [];              // "object"
typeof null;            // "object" (historical bug)
typeof function(){};    // "function" (special case)

That null quirk is a legacy mistake in JavaScript. Despite typeof null returning "object", null is a primitive.

The Seven Primitive Types

Primitives are the atomic values in JavaScript. They’re immutable—you can’t change them, only replace them. They’re compared by value, meaning two primitives are equal if they contain the same value.

// String: textual data
let name = "Alice";
let greeting = 'Hello';
let template = `Hi, ${name}`;

// Number: floating-point numbers (IEEE 754)
let integer = 42;
let decimal = 3.14;
let negative = -10;
let infinity = Infinity;
let notANumber = NaN;

// BigInt: arbitrary-precision integers (ES2020)
let big = 9007199254740991n;
let bigFromConstructor = BigInt("9007199254740991");

// Boolean: true or false
let isActive = true;
let isComplete = false;

// Undefined: declared but not assigned
let uninitialized;
console.log(uninitialized); // undefined

// Null: intentional absence of value
let emptyValue = null;

// Symbol: unique identifiers (ES2015)
let id1 = Symbol("id");
let id2 = Symbol("id");
console.log(id1 === id2); // false (always unique)

Immutability means primitives can’t be altered:

let str = "hello";
str[0] = "H";  // Fails silently in non-strict mode
console.log(str); // "hello" (unchanged)

// Creating a new string works
str = "H" + str.slice(1);
console.log(str); // "Hello"

Value comparison is straightforward:

let a = 5;
let b = 5;
console.log(a === b); // true

let x = "test";
let y = "test";
console.log(x === y); // true

Objects and Reference Types

Everything that isn’t a primitive is an object. Objects store collections of properties and are mutable. They’re compared by reference, not value—two objects are only equal if they point to the same location in memory.

// Object literal notation
let user = {
  name: "Bob",
  age: 30
};

// Constructor notation
let date = new Date();

// Object.create
let person = Object.create(null);
person.name = "Charlie";

// Arrays are objects
let numbers = [1, 2, 3];
console.log(typeof numbers); // "object"

// Functions are objects
function greet() {}
console.log(typeof greet); // "function"
greet.customProperty = "yes"; // Can add properties

Reference comparison creates gotchas:

let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;

console.log(obj1 === obj2); // false (different references)
console.log(obj1 === obj3); // true (same reference)

obj3.value = 20;
console.log(obj1.value); // 20 (obj1 and obj3 point to same object)

Copying objects requires care:

// Shallow copy with spread operator
let original = { a: 1, b: { c: 2 } };
let shallow = { ...original };

shallow.a = 99;
console.log(original.a); // 1 (primitive copied by value)

shallow.b.c = 99;
console.log(original.b.c); // 99 (nested object still referenced)

// Deep copy (simple approach)
let deep = JSON.parse(JSON.stringify(original));
deep.b.c = 100;
console.log(original.b.c); // 99 (truly independent)

// Note: JSON approach fails with functions, undefined, symbols, dates

Type Coercion and Conversion

JavaScript aggressively coerces types in mixed-type operations. This causes bugs if you don’t expect it.

// Implicit coercion
console.log("5" + 1);     // "51" (number to string)
console.log("5" - 1);     // 4 (string to number)
console.log("5" * "2");   // 10 (both strings to numbers)
console.log(true + 1);    // 2 (true becomes 1)
console.log(false + 1);   // 1 (false becomes 0)

Truthy and falsy values matter in conditionals:

// Falsy values: false, 0, -0, 0n, "", null, undefined, NaN
// Everything else is truthy

if ("") console.log("won't run");
if ("0") console.log("will run"); // Non-empty string is truthy
if ([]) console.log("will run");  // Empty array is truthy
if ({}) console.log("will run");  // Empty object is truthy

Explicit conversion is clearer:

// String conversion
String(123);        // "123"
String(true);       // "true"
String(null);       // "null"
String(undefined);  // "undefined"

// Number conversion
Number("123");      // 123
Number("12.3");     // 12.3
Number("12abc");    // NaN
Number(true);       // 1
Number(false);      // 0
Number(null);       // 0
Number(undefined);  // NaN

// Boolean conversion
Boolean(1);         // true
Boolean(0);         // false
Boolean("hi");      // true
Boolean("");        // false
Boolean({});        // true

Use strict equality to avoid coercion:

console.log(0 == false);   // true (coercion)
console.log(0 === false);  // false (no coercion)

console.log("" == 0);      // true (coercion)
console.log("" === 0);     // false (no coercion)

console.log(null == undefined);   // true (special case)
console.log(null === undefined);  // false

Wrapper Objects and Auto-boxing

Primitives don’t have methods, yet "hello".toUpperCase() works. JavaScript temporarily wraps primitives in object wrappers when you access properties or methods.

let str = "hello";
console.log(str.toUpperCase()); // "HELLO"

// Behind the scenes:
// 1. new String("hello") creates temporary wrapper
// 2. .toUpperCase() is called on wrapper
// 3. Wrapper is discarded, primitive result returned

You can create wrapper objects explicitly, but don’t:

let primitiveStr = "test";
let objectStr = new String("test");

console.log(typeof primitiveStr); // "string"
console.log(typeof objectStr);    // "object"

console.log(primitiveStr === "test");  // true
console.log(objectStr === "test");     // false

// Wrapper objects are truthy even when wrapping falsy values
if (new Boolean(false)) {
  console.log("This runs!"); // Runs because object is truthy
}

Never use new String(), new Number(), or new Boolean(). Use the functions without new for explicit conversion instead.

Practical Implications and Best Practices

Understanding types affects real code decisions. Here’s how to check types properly:

// typeof works for primitives and functions
typeof "hi";           // "string"
typeof 42;             // "number"
typeof true;           // "boolean"
typeof undefined;      // "undefined"
typeof Symbol();       // "symbol"
typeof 10n;            // "bigint"
typeof function(){};   // "function"

// typeof fails for null and specific object types
typeof null;           // "object" (bug)
typeof [];             // "object" (not helpful)
typeof {};             // "object" (not helpful)

// instanceof checks prototype chain
[] instanceof Array;           // true
[] instanceof Object;          // true
({}) instanceof Object;        // true
new Date() instanceof Date;    // true

// Most reliable for objects
Object.prototype.toString.call([]);        // "[object Array]"
Object.prototype.toString.call({});        // "[object Object]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(null);      // "[object Null]"

Performance considerations:

// Primitives are faster
let primitive = "test";
let wrapper = new String("test");

// Primitive operations are optimized
console.time("primitive");
for (let i = 0; i < 1000000; i++) {
  let x = "hello";
  x.toUpperCase();
}
console.timeEnd("primitive");

// Wrapper objects add overhead
console.time("wrapper");
for (let i = 0; i < 1000000; i++) {
  let x = new String("hello");
  x.toUpperCase();
}
console.timeEnd("wrapper");

Best practices:

  1. Use primitives by default. Only create objects when you need mutable collections.
  2. Use strict equality (===). Avoid == unless you specifically need coercion.
  3. Explicitly convert types. Use Number(), String(), Boolean() instead of relying on coercion.
  4. Check types defensively. Use typeof for primitives, Array.isArray() for arrays, instanceof for custom classes.
  5. Understand reference behavior. When passing objects to functions, remember you’re passing a reference—mutations affect the original.
function processUser(user) {
  user.processed = true; // Mutates original object
}

let myUser = { name: "Dave" };
processUser(myUser);
console.log(myUser.processed); // true

// To avoid mutation, clone first
function processUserSafe(user) {
  let copy = { ...user };
  copy.processed = true;
  return copy;
}

JavaScript’s type system is quirky, but predictable once you internalize these rules. Primitives are immutable values, objects are mutable references, and the language will coerce between them unless you use strict comparisons. Master these fundamentals and you’ll write fewer bugs and debug faster.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.