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.
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
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");
}
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
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
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.
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
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.
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
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:
value: the property’s stored datawritable: whether the value can be changedenumerable: whether it appears in loops and enumerationsconfigurable: whether the property can be deleted or modified
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)
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
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
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"
"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).
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
}
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.
+ 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.
[] == ![] 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.
value != null with type checks to safely exclude both null and undefined before accessing properties.