<< Home

Chapter 3: Types and Values

Everything in JavaScript is a value, and every value has a type. Understanding the difference between types and values, and how JavaScript interprets and converts them, is essential to writing clear and predictable code. Unlike many strictly typed languages, JavaScript uses dynamic typing, which means that variables are not bound to any particular type. A single variable can hold different kinds of values during execution, and the language automatically converts between types when necessary.

JavaScript’s type system includes a set of primitive types for representing basic data, and a single non-primitive type, the object, which acts as a container for collections of values and more complex structures. You will also encounter operators and constructs that help you identify, compare, and safely convert between these types. Together, these concepts define how values behave, how they interact with one another, and how they are stored and referenced in memory.

💡 JavaScript’s dynamic typing makes it flexible, but also prone to subtle bugs if conversions are not clearly understood. Being deliberate about how values are compared and coerced helps prevent logic errors.

This chapter explores the fundamental types and their characteristics, how values are stored and compared, and how JavaScript performs type coercion under the hood. You will learn to use operators such as typeof and instanceof effectively, understand equality distinctions, and handle tricky conversions safely and predictably.

Primitives: undefined, null, boolean, number

Primitive types are the simplest forms of data in JavaScript. They are immutable values that are stored directly, not as references, and include undefined, null, boolean, number, string, symbol, and bigint. This section introduces the first four of these core primitives, which are fundamental to all JavaScript programs.

undefined

The special value undefined represents a variable that has been declared but not assigned a value. It is also the default return value of functions that do not explicitly return anything. Accessing a property or element that does not exist also results in undefined.

let x;
console.log(x);      // undefined

function test() {}
console.log(test()); // undefined
⚠️ Do not confuse an uninitialized variable (undefined) with a missing variable, which causes a ReferenceError.

null

The value null represents the intentional absence of any object value. It is often used to indicate that a variable should be empty or that a reference points to nothing.

let user = null;
if (user === null) {
  console.log("No user data available");
}
💡 Historically, typeof null returns "object" due to a design bug that remains for backward compatibility. Always check for null using strict equality (===) instead.

boolean

Boolean values are either true or false. They commonly appear as results of comparisons or conditions in control statements. JavaScript can also convert other types to booleans when used in logical expressions, based on their “truthy” or “falsy” nature.

let active = true;
let ready = false;

console.log(Boolean(0));      // false
console.log(Boolean("Hi"));   // true
⚠️ The following values are falsy: false, 0, -0, 0n, "", null, undefined, and NaN. Everything else is truthy.

number

JavaScript uses a single numeric type, number, which represents both integers and floating-point values using the IEEE 754 double-precision format. This means that certain arithmetic operations can introduce rounding errors.

let x = 10;
let y = 3.5;
let z = x / y;

console.log(z); // 2.857142857142857

Special numeric values include Infinity, -Infinity, and NaN (Not-a-Number). These are valid number values and can result from invalid or overflow operations.

console.log(1 / 0);       // Infinity
console.log(-1 / 0);      // -Infinity
console.log("abc" * 2);   // NaN
💡 Use Number.isNaN(value) to check for NaN, since NaN === NaN is always false.

string, symbol, bigint

In addition to numbers and booleans, JavaScript provides several other primitive types that handle text, unique identifiers, and very large integers. These are string, symbol, and bigint. Each serves a specific purpose and behaves differently from objects, even though they can sometimes appear similar when used in expressions or converted implicitly.

string

A string represents a sequence of characters enclosed in single quotes, double quotes, or backticks. Strings are immutable, meaning once created they cannot be changed. Any operation that appears to modify a string actually produces a new one. Template literals, introduced in ES6, use backticks (`) and support interpolation with ${...} expressions.

let name = "Robin";
let greeting = `Hello, ${name}!`;
console.log(greeting); // Hello, Robin!

Strings can be concatenated with the + operator or manipulated using built-in methods such as slice(), toUpperCase(), and includes(). They are commonly used to represent textual data, keys, and serialized information.

💡 Use template literals for readability and to safely embed variables or expressions in text without manual concatenation.

symbol

The symbol type represents a unique and immutable value, often used as an identifier for object properties. Each call to Symbol() creates a new value that is guaranteed to be distinct, even if given the same description. Symbols do not coerce to strings, which helps prevent accidental key collisions in objects.

const id = Symbol("id");
const user = {
  name: "Alice",
  [id]: 123
};

console.log(user[id]);  // 123
⚠️ Symbols are not enumerable in for...in loops or returned by Object.keys(). To access them, use Object.getOwnPropertySymbols().

bigint

The bigint type allows you to represent integers larger than the maximum safe value for number (253 - 1). A bigint is created by appending an n to the end of an integer literal or by calling BigInt().

const large = 9007199254740993n;
console.log(large + 2n); // 9007199254740995n

Unlike number, bigint values cannot be mixed directly with non-bigint values in arithmetic expressions. Attempting to do so will cause a TypeError. They are primarily used when working with very large counts, identifiers, or precise integer arithmetic.

💡 Use BigInt() carefully, as some APIs and libraries may not yet support bigint values.

objects and property attributes

Unlike primitive values, which are immutable and stored directly, an Object is a mutable collection of key–value pairs. Keys are usually strings or symbols, and values can be any type, including functions and other objects. Most of JavaScript’s complex data structures, such as arrays, maps, and functions, are specialized kinds of Objects.

const person = {
  name: "Sam",
  age: 30
};

person.job = "Developer";
console.log(person.name);  // Sam

Objects are passed by reference, not by value. This means that if two variables reference the same object, modifying it through one variable also affects the other.

const a = { x: 1 };
const b = a;

b.x = 2;
console.log(a.x);  // 2
⚠️ Copying an Object by assignment only copies its reference. To clone an object, use structuredClone(), Object.assign(), or spread syntax ({ ...obj }).

Property attributes

Each property of an Object has internal attributes that define its behavior. These include:

These attributes can be inspected or defined explicitly using Object.getOwnPropertyDescriptor() and Object.defineProperty().

const user = {};
Object.defineProperty(user, "id", {
  value: 42,
  writable: false,
  enumerable: true,
  configurable: false
});

console.log(user.id);  // 42
user.id = 99;          // silently ignored (or error in strict mode)
💡 Non-enumerable properties do not appear in for...in loops or when spreading ({ ...obj }). Use descriptors to create hidden or read-only fields safely.

Accessors: getters and setters

In addition to data properties, objects can define accessor properties using get and set functions. These allow computed or controlled access to internal data without exposing it directly.

const product = {
  _price: 100,
  get price() {
    return `$${this._price}`;
  },
  set price(value) {
    if (value > 0) this._price = value;
  }
};

product.price = 150;
console.log(product.price); // $150
⚠️ Accessor properties do not have a value or writable attribute. Instead, their behavior is defined by their get and set functions.

Prototype chain

Every Object has an internal link to another object known as its prototype. When accessing a property that does not exist directly on the object, JavaScript looks for it along this prototype chain. This mechanism enables inheritance and shared behavior across object types.

const base = { kind: "base" };
const derived = Object.create(base);

console.log(derived.kind);                             // "base"
console.log(Object.getPrototypeOf(derived) === base);  // true
💡 Use Object.create(null) to create an object with no prototype, useful for pure key–value maps without inherited properties.

typeof, instanceof, and equality

JavaScript provides several operators to identify the type or class of a value and to compare values for equality. Understanding how these operators behave is crucial, especially since some may produce results that seem counterintuitive due to JavaScript’s dynamic typing and type coercion rules.

typeof

The typeof operator returns a string describing the type of its operand. It works for both primitives and objects, though it has a few historical quirks.

console.log(typeof 123);        // "number"
console.log(typeof "Hello");    // "string"
console.log(typeof true);       // "boolean"
console.log(typeof undefined);  // "undefined"
console.log(typeof null);       // "object" (legacy bug)
console.log(typeof {});         // "object"
console.log(typeof Symbol());   // "symbol"
⚠️ The result "object" for null is a long-standing language bug preserved for backward compatibility. Use strict checks like value === null when you specifically need to detect null.

instanceof

The instanceof operator tests whether an object’s prototype chain includes the prototype property of a given constructor function. It is useful for checking the class of objects and custom types.

function Person(name) {
  this.name = name;
}

const alice = new Person("Alice");

console.log(alice instanceof Person);  // true
console.log(alice instanceof Object);  // true

This operator only works with objects, not primitives, and relies on the internal prototype chain, so it may not behave as expected when dealing with objects across different JavaScript contexts (such as iframes or workers).

💡 Use value.constructor === Type or Object.prototype.toString.call(value) for more predictable type checks, especially when working with multiple execution contexts.

Equality operators

JavaScript has two sets of equality operators: loose equality (==) and strict equality (===). The loose version performs type coercion before comparison, while the strict version does not.

console.log(5 == "5");           // true
console.log(5 === "5");          // false
console.log(null == undefined);  // true
console.log(null === undefined); // false

Using === and !== is generally safer, since they prevent unexpected conversions. Only use == when you explicitly want coercion between compatible types, such as comparing null and undefined.

⚠️ NaN is never equal to anything, including itself. Use Number.isNaN() to test for it.

Object identity

Objects are compared by reference, not by value. Two distinct objects are never equal, even if their properties have identical contents.

const a = { x: 1 };
const b = { x: 1 };
const c = a;

console.log(a === b);  // false
console.log(a === c);  // true

To compare object contents, you must check individual properties manually or use utilities such as structuredClone() combined with serialization, or libraries that perform deep comparisons.

💡 Object.is() provides a more accurate comparison for some edge cases, such as distinguishing +0 from -0 and correctly treating NaN as equal to itself.

Coercion rules and safe checks

Type coercion is the process by which JavaScript automatically converts values from one type to another when required by an operation. This happens in many contexts, such as comparisons, arithmetic, and string concatenation. While coercion allows flexible code, it can also introduce subtle bugs if not fully understood.

Implicit coercion

JavaScript performs implicit conversions when combining values of different types. For example, when a string and a number are used together with the + operator, the number is converted to a string. In contrast, other arithmetic operators (-, *, /) convert both operands to numbers.

console.log("5" + 2);    // "52"
console.log("5" - 2);    // 3
console.log("5" * "2");  // 10

Boolean contexts also trigger coercion. Values like 0, "", null, undefined, and NaN become false, while all others become true.

if ("hello") {
  console.log("This runs"); // non-empty string is truthy
}
⚠️ Implicit coercion can make code unpredictable. Always be aware of whether an operation expects a number, string, or boolean result before relying on coercion.

Explicit coercion

Explicit coercion occurs when you deliberately convert a value to another type using functions or constructors like String(), Number(), Boolean(), or BigInt(). These make conversions clear and intentional.

let n = Number("42");  // 42
let s = String(42);    // "42"
let b = Boolean(0);    // false

When precision matters, always prefer explicit conversions to make your intent obvious and avoid automatic type juggling.

💡 The unary + operator is a concise way to convert a value to a number: +"5" becomes 5. Use sparingly for clarity.

Loose equality and coercion

The loose equality operator (==) triggers coercion when the operands differ in type. Some of its rules are non-intuitive and can lead to errors if used without care.

console.log(false == 0);        // true
console.log("" == 0);           // true
console.log(null == undefined); // true
console.log([] == false);       // true

For predictable comparisons, always use strict equality (===) instead, which compares both type and value without conversion.

⚠️ A common source of confusion is that [] == ![] evaluates to true due to the multi-step coercion process. Avoid constructs that rely on implicit truthy/falsy conversions in equality checks.

Safe type checking

When working in a dynamically typed environment, it is often necessary to confirm that a value has the expected type before performing operations. The safest approaches combine typeof, Array.isArray(), and constructor checks.

if (typeof value === "string") {
  console.log("It is a string");
}

if (Array.isArray(value)) {
  console.log("It is an array");
}

if (value instanceof Date) {
  console.log("It is a Date object");
}

Always prefer clear checks over assumptions, especially when handling user input, API responses, or loosely structured data.

💡 For defensive programming, combine value != null with type checks to safely exclude both null and undefined before accessing properties.