Site icon Ampersand Tutorials

JavaScript Variables and Data Types (2026)

javascript data types tutorial cover showing 'JavaScript Variables and Data Types (2026)' in bold white lettering on a dark navy grid background.

Quick answer: JavaScript data types come in eight kinds — string, number, bigint, boolean, undefined, null, symbol and object — and you declare them with three keywords: const, let and the older var. Use const by default and let only when the value must be reassigned; avoid var. string, number, bigint, boolean, undefined, null, symbol and object.

const, let and var

const name = "Ada";      // cannot be reassigned
let count = 0;           // can be reassigned
count = count + 1;

var old = "avoid";       // function-scoped, legacy
const list = [1, 2];
list.push(3);            // allowed — contents changed, binding unchanged
// list = [];            // TypeError — cannot reassign a const

The primitive JavaScript data types

TypeExamplesNotes
string"hello", 'hi', backticksBackticks allow interpolation
number42, 3.14, NaNOne type for integers and decimals
bigint9007199254740993nFor integers beyond the safe range
booleantrue, falseResult of comparisons
undefined—Declared but not assigned
null—Deliberately empty
symbolSymbol("id")A unique key
object{}, [], functionsEverything that is not a primitive

number being one type is worth remembering: 1 === 1.0 is true, integer division produces decimals (7 / 2 is 3.5), and very large values lose precision, which is why bigint exists.

Strings and template literals

const user = "Ada";
const greeting = `Hello, ${user}!`;      // template literal
const multi = `line one
line two`;                                // real multiline strings
const { length } = user;                  // 3

Prefer backticks with ${...} over concatenation with +. Interpolation keeps the message readable and avoids accidental type coercion when a value is not a string.

JavaScript data types: truthy, falsy and the equality question

Some values are treated as false in a condition. The list is short and worth memorising: false, 0, -0, 0n, "", null, undefined, NaN. Everything else is truthy — including empty arrays and empty objects, which surprises people.

if ([]) console.log("runs");        // empty array is truthy
if (0)   console.log("skipped");    // 0 is falsy

"" == 0        // true  — loose equality coerces
"" === 0       // false — strict equality does not

null == undefined   // true
null === undefined  // false

Use === and !== almost always. Loose == performs type coercion with a long rule table, and the only place it beats strict equality is the deliberate value == null check, which matches both null and undefined at once.

Number and string conversion between JavaScript data types

Number("42")        // 42
Number("")          // 0
parseInt("42px", 10) // 42 — stops at the first non-digit
String(42)          // "42"
(42).toFixed(2)     // "42.00"
[1,2,3].join(", ")  // "1, 2, 3"

Prefer explicit Number() and String() over relying on implicit coercion. And remember that NaN is the only value not equal to itself: test it with Number.isNaN(value), never with ===.

Treat the rules of JavaScript data types as habits rather than trivia, and the surprises stop arriving as bugs. Next in the series: functions. To learn JavaScript properly with a mentor, Ampersand Academy teaches web development one-to-one.

JavaScript data types: mistakes and debugging habits

Most JavaScript data types bugs are comparisons, not declarations. The language has only eight primitives, yet the errors that consume an afternoon almost always come from comparing two of them incorrectly. Strict equality is the default habit for good reason: it never converts, so the answer tells you whether the values are genuinely the same. Loose equality quietly coerces across types, and while the coercion rules are learnable, applying them under deadline pressure is where the JavaScript data types you thought you were comparing turn out to be a string and a number that only look alike.

Truthy and falsy values cause the next tier of confusion. Beyond the six falsy primitives — false, 0, the empty string, null, undefined and NaN — everything else is truthy, including the strings “0” and “false” and the number NaN’s close cousin of an empty array. That means a guard such as if (value) on a parameter that may legitimately be zero takes the wrong branch, and the bug surfaces far from its cause. Prefer an explicit comparison when the data type could be a number: checking value !== undefined or value !== 0 states the real condition instead of hoping for a convenient coercion.

Two numeric traps are worth memorising because they corrupt data silently. The first is NaN, the result of an invalid numeric operation, which is the only value in JavaScript not equal to itself, so identity checks against it always fail; only Number.isNaN() recognises it. The second is floating point arithmetic, where 0.1 + 0.2 is famously not 0.3. Neither is a syntax problem, and both surface as wrong totals in reports. When you compare money, round deliberately at the edges of the program with integer cents or a decimal library, and keep the JavaScript data types you store for money away from binary floating point altogether.

Reference types behave differently from primitives under assignment, and this distinction explains a whole family of bugs. Primitives are copied by value, so changing one variable never touches another. Objects, arrays and functions are shared by reference, so two variables pointing at the same array see each other’s changes, and a function that appears to take a defensive copy by spreading the top level still shares every nested object. When you need a genuine copy, decide whether a shallow clone suffices or whether you need structured deep cloning, and be explicit in the code about which one you chose. Silent sharing across a large application is one of the hardest JavaScript data types issues to trace.

Debugging habits help more than memorising tables. When a value misbehaves, log its type alongside the value itself: console.log(typeof value, value) immediately separates a missing property from a wrongly shaped one, and for structured data the inspector’s variable view beats any print statement. Keep a single convention for where nullable values come from — either a function always returns a real value or it returns null and the caller checks — because mixing the two styles is what produces the defensive checks scattered through otherwise clean code. Consistent JavaScript data types conventions turn a class of mysteries into a class of compile-time or first-line checks.

Accurate runtime inspection of javascript data types

Distinguishing values reliably at runtime demands more than the basic typeof operator. While typeof works predictably for strings, booleans, and numbers, it famously returns "object" for null. This historic quirk creates subtle regressions when validating unknown input payloads. To correctly classify various javascript data types entering an API boundary, developers must combine primitive operators with prototype inspection. Checking for missing values requires a dedicated guard before attempting property access or method invocation.

Arrays also evaluate to "object" under standard operator checks, making structural validation unreliable without dedicated methods. Modern environments provide Array.isArray() to cleanly resolve whether an incoming collection is a list rather than a plain dictionary. For arbitrary values where precision is paramount, borrowing Object.prototype.toString provides the internal class tag. This technique distinguishes ordinary objects from regular expressions, dates, maps, sets, and error instances without relying on fragile constructor names that change during minification.

function identifyType(value) {
  if (value === null) return "null";
  if (typeof value !== "object") return typeof value;
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

identifyType([]); // "array"
identifyType(new Date()); // "date"
identifyType(/regex/); // "regexp"

function identifyType(value) { if (value === null) return "null"; if (typeof value !== "object") return typeof value; return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); } identifyType([]); // "array" identifyType(new Date()); // "date" identifyType(/regex/); // "regexp"

Using a centralized utility guarantees uniform behavior across an entire application codebase. When parsing external JSON or querying third-party services, classifying incoming javascript data types through tag extraction prevents invalid assumptions before business logic executes. This pattern avoids defensive blocks scattered across route handlers. It also exposes corrupted payloads early, producing clear runtime errors instead of silent failures downstream.

Another common point of failure involves host objects in browser and Node.js environments. Window objects, document nodes, and streaming buffers do not always adhere to standard prototype hierarchies. Relying on simple instance checks against local globals can fail when objects cross iframe boundaries or worker threads. Extracting the internal class string bypasses environment boundaries, providing a dependable foundation for input sanitization pipelines.

Deep cloning and mutation guards for structured data

Handling mutable structures safely requires understanding how memory references operate. Primitives travel by value, meaning modifications to copied variables never mutate the original source. In contrast, complex javascript data types such as objects and arrays pass references to their underlying memory allocations. Assigning an existing object to a fresh variable creates an alias rather than a duplicate. If a downstream service alters a nested property on that alias, every upstream reference reflects the change immediately.

Shallow copies versus structuredClone

Shallow copying mechanisms like Object.assign() or the spread operator address only the first layer of key-value pairs. Nested objects, arrays, and date instances remain shared between the original structure and the copy. Mutating a nested configuration setting inside a spread clone will corrupt the original object. Prior to native browser support, solving this problem required heavy external dependencies or risky serialization hacks with JSON.parse() and JSON.stringify().

Native runtimes now provide structuredClone(), which performs true deep copies across nested trees while preserving complex structures. Unlike JSON serialization, it correctly duplicates regular expressions, dates, typed arrays, and even cyclic references where an object points back to itself. However, copying certain complex javascript data types with structuredClone() will throw an error if the payload contains functions, DOM nodes, or symbol-keyed properties.

const profile = {
  id: 101,
  preferences: { theme: "dark" },
  created: new Date()
};

const deepCopy = structuredClone(profile);
deepCopy.preferences.theme = "light";
console.log(profile.preferences.theme); // "dark"

const profile = { id: 101, preferences: { theme: "dark" }, created: new Date() }; const deepCopy = structuredClone(profile); deepCopy.preferences.theme = "light"; console.log(profile.preferences.theme); // "dark"

Choosing the right copying mechanism depends on the internal shapes of your javascript data types and performance requirements. Shallow copies via object spread remain appropriate for flat state objects where properties are primitive values. For deeply nested application state, configuration trees, or untrusted incoming requests, structuredClone() delivers robust isolation without external dependencies. Reserving deep clones for boundaries prevents unnecessary performance overhead in hot execution loops.

When dealing with unsupported values like closures or class methods, you must decide whether to strip those fields or implement an explicit domain mapper. Attempting to clone instances with active network handles or open file descriptors will trigger immediate serialization exceptions. Explicit mapping keeps domain entities separate from plain persistence records, ensuring that only serializable properties traverse architectural boundaries.

Symbol and BigInt edge cases in modern applications

Modern standards introduced BigInt and Symbol to address longstanding gaps in numeric precision and property collision. While these additions solved critical architecture challenges, they introduced distinct operational boundaries compared to classic javascript data types found in early specifications. Understanding these boundaries prevents unexpected runtime exceptions when performing arithmetic calculations, serializing records, or iterating through dictionary structures.

The primary pitfall with BigInt involves implicit type mixing. JavaScript deliberately refuses to coerce BigInt and standard Number primitives within mathematical operations. Evaluating 42n + 1 throws a TypeError rather than producing an approximated sum. To execute arithmetic between mixed numeric values, developers must explicitly convert one operand using either Number() or BigInt(). Beware that casting large integers into standard numbers risks truncation once numbers exceed safe integer limits.

Symbols serve as guaranteed unique property keys, eliminating naming conflicts between separate modules sharing an object. However, symbol properties do not appear during standard iteration. Constructs like for...in loops, Object.keys(), and JSON.stringify() overlook symbol keys completely. Inspecting symbol properties requires dedicated reflection methods like Object.getOwnPropertySymbols() or Reflect.ownKeys(). This behavior makes symbols ideal for internal metadata, but unsuitable for fields that require routine JSON serialization.

Another serialization caveat involves standard JSON.stringify() processing. Attempting to serialize an object containing a BigInt value throws an unhandled TypeError unless a custom replacer function or .toJSON() method is defined on the prototype. When designing data access layers that store 64-bit database identifiers, treat these specialized javascript data types with deliberate conversion strategies before handing payloads to network clients.

Well-known symbols like Symbol.iterator, Symbol.toPrimitive, and Symbol.hasInstance unlock powerful metaprogramming capabilities. By attaching these hooks to domain classes, you can customize how user-defined structures respond to coercion, iteration protocols, and instance checks. Leveraging well-known symbols allows custom collection types to integrate seamlessly with native language constructs like for...of loops and destructuring assignments.

Defensive immutability with javascript data types

Declaring a variable with const protects the identifier binding from being reassigned, but it provides zero protection against mutating the contents of an object or array. Building reliable architectures often requires true immutability to prevent unexpected side effects across modular applications. By mastering native immutability utilities, teams can harden internal state structures across different javascript data types without loading bulky runtime libraries.

Freezing versus sealing objects

The standard library offers two primary tools for restricting object modifications: Object.seal() and Object.freeze(). Calling Object.seal() prevents new properties from being added and existing properties from being deleted, but existing writable properties can still be modified. This level of restriction works well for established configuration records where values may update, but the schema must remain fixed against arbitrary property additions.

In contrast, Object.freeze() establishes strict immutability by making every existing property non-writable and non-configurable. In strict mode, any attempt to add, delete, or reassign a property throws a TypeError. However, Object.freeze() is shallow by default. If a frozen object contains nested arrays or child dictionaries, those internal children remain fully mutable. Truly protecting nested javascript data types requires implementing a recursive freeze function across all child objects.

Immutable data structures streamline debugging by ensuring that functions treat input parameters as read-only contracts. When a function receives a frozen object, team members can rest assured that passing that object through multiple transformation steps will never modify the original record. Enforcing immutability at domain boundaries dramatically reduces regression rates in large-scale applications where shared state is common.

Performance trade-offs should guide the adoption of defensive immutability. Freezing large object graphs during request processing consumes processor cycles and prevents JavaScript virtual machine optimizations like hidden class transitions. Apply deep freezing primarily in development environments to catch unauthorized mutations during automated testing. In production builds, lightweight shallow freezes at architectural borders deliver sufficient safety without degrading throughput.

Preventing memory leaks with weak collections

Managing long-lived web applications requires vigilance around garbage collection lifecycles. Standard collections like Map and Set maintain strong references to every key and value stored inside them. As long as the parent collection exists in memory, none of its stored javascript data types can be reclaimed by the garbage collector, even if all other application references have been deleted. This retained memory frequently produces subtle leaks in event-driven systems.

To resolve reference retention problems, the language provides weak collection variants: WeakMap and WeakSet. These structures hold references weakly, meaning their presence in the collection does not prevent the JavaScript runtime engine from reclaiming the target object during garbage collection. Understanding how weak references interact with standard javascript data types allows engineers to attach private metadata to objects without risking long-term memory bloat.

const requestMetadata = new WeakMap();

function trackRequest(requestObj, details) {
  requestMetadata.set(requestObj, details);
}

// When requestObj falls out of scope, metadata is collected automatically

const requestMetadata = new WeakMap(); function trackRequest(requestObj, details) { requestMetadata.set(requestObj, details); } // When requestObj falls out of scope, metadata is collected automatically

A critical characteristic of WeakMap is that its keys must always be objects or non-registered symbols, never primitive values like strings or numbers. Primitives have no distinct object identities, so weak reference semantics cannot apply to them. Furthermore, weak collections cannot be iterated; there are no .keys(), .values(), or .size properties available. This intentional limitation ensures that code cannot inspect the internal state of garbage collection timing.

By applying weak collections for caches, event listeners, and private instance fields, developers keep memory consumption predictable throughout extended user sessions. Knowing when to use strong versus weak references is an essential skill for managing complex javascript data types in production environments. Choosing the appropriate reference strategy ensures high-performance client experiences without manual cleanup overhead.

Modern memory management also benefits from FinalizationRegistry when integrating with WebAssembly modules or native operating system resources. While weak collections quietly discard references upon garbage collection, finalizers let you execute cleanup callbacks after objects are reclaimed. Combining weak collections with finalizers provides an end-to-end strategy for resource tracking across complex client architectures.

Frequently asked questions

When should I use const instead of let in JavaScript?

Use const by default. Switch to let only when you must reassign the variable, such as a counter. const still allows you to change an array’s or object’s contents; it only prevents rebinding.

What is the difference between == and === in JavaScript?

=== compares value and type without coercion. == coerces the operands to a common type first, which produces surprising results such as empty string equalling zero. Prefer === and !==.

What are the falsy values in JavaScript?

false, 0, -0, 0n, empty string, null, undefined and NaN. Everything else is truthy, including empty arrays and empty objects, which often surprises beginners.

Why is typeof null equal to object?

It is a long-standing bug from the language’s first version that cannot be fixed without breaking the web. Check for null explicitly with value === null instead of relying on typeof.

How do I check for NaN in JavaScript?

Use Number.isNaN(value). NaN is the only value not equal to itself, so value === NaN is always false and will never detect it.

Exit mobile version