JavaScript Basics: Variables, Data Types, and Operators

JavaScript-Basics-Variables-Data-Types-and-Operators

Every single JavaScript concept I’ve learned since builds on top of variables, data types, and operators — they’re genuinely the atoms of the language. I still remember getting tripped up early on by things like "5" + 3 producing "53" instead of 8, and it wasn’t until I understood JavaScript’s type system properly that these surprises stopped happening. In this article, I want to give you the same solid foundation.

Declaring Variables: var, let, const

var oldStyle = "I'm function-scoped";
let modern = "I'm block-scoped and reassignable";
const constant = "I'm block-scoped and cannot be reassigned";

modern = "changed"; // fine
constant = "changed"; // TypeError: Assignment to constant variable

I use const by default for everything, and only switch to let when I know a variable’s value genuinely needs to change later. I avoid var entirely in modern code because of its function-scoping quirks and hoisting behavior, which cause bugs that block-scoped let/const simply don’t have.

const Doesn’t Mean Immutable

This confused me early on — const prevents reassignment of the variable itself, but if the value is an object or array, its contents can still be modified.

const user = { name: "Alice" };
user.name = "Bob"; // totally fine — modifying a property, not reassigning the variable
console.log(user.name); // Bob

user = { name: "Carol" }; // TypeError — this IS a reassignment

The Temporal Dead Zone

console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;

Unlike var (which is hoisted and initialized to undefined), let and const are hoisted but remain uninitialized until their declaration line actually executes. This period is called the Temporal Dead Zone (TDZ), and it’s actually a helpful safety feature — it turns what would silently be undefined with var into a loud, immediate error with let/const.

JavaScript’s Data Types

JavaScript has two broad categories of types: primitives and objects.

Primitive Types

TypeExampletypeof Result
String"hello""string"
Number42, 3.14"number"
BigInt123n"bigint"
Booleantrue, false"boolean"
Undefinedundefined"undefined"
Nullnull"object" (a famous, long-standing quirk)
SymbolSymbol("id")"symbol"
console.log(typeof "hello");    // string
console.log(typeof 42);         // number
console.log(typeof true);       // boolean
console.log(typeof undefined);  // undefined
console.log(typeof null);       // object — this is a well-known bug baked into the language since 1995
console.log(typeof Symbol());   // symbol
console.log(typeof 10n);        // bigint

Primitives are immutable and compared by value — two primitives with the same value are always considered equal.

console.log("hello" === "hello"); // true
console.log(5 === 5);              // true

The Object Type

Everything that isn’t a primitive is an object — including plain objects, arrays, functions, dates, maps, and sets. Objects are compared by reference, not by value.

console.log(typeof {});         // object
console.log(typeof []);         // object — arrays are technically objects
console.log(typeof function(){}); // function — a special case, though functions are objects too

console.log({} === {}); // false — different references, even with identical (empty) content

Number Type Details

JavaScript has only one number type, representing both integers and floating-point numbers using the IEEE 754 double-precision format.

console.log(0.1 + 0.2);           // 0.30000000000000004 — classic floating-point precision issue
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(1 / 0);               // Infinity
console.log(-1 / 0);              // -Infinity
console.log(0 / 0);               // NaN
console.log(typeof NaN);          // number (yes, NaN is technically a number!)
console.log(NaN === NaN);         // false — NaN is never equal to itself
console.log(Number.isNaN(NaN));   // true — the reliable way to check for NaN

For values beyond Number.MAX_SAFE_INTEGER, or when exact large-integer precision matters (like financial calculations or unique IDs), I use BigInt:

const big = 9007199254740993n;
console.log(big + 1n); // 9007199254740994n
// console.log(big + 1); // TypeError: Cannot mix BigInt and other types

String Type Details

const single = 'Hello';
const double = "World";
const template = `${single}, ${double}!`;

console.log(template.length);       // 12
console.log(template.toUpperCase()); // HELLO, WORLD!
console.log(template.includes("World")); // true
console.log(template.slice(0, 5));  // Hello

Strings are immutable — every string method returns a new string rather than modifying the original.

const str = "hello";
str.toUpperCase();
console.log(str); // still "hello" — the original was never changed

Type Coercion

This is where a lot of JavaScript’s reputation for “weirdness” comes from, but it becomes entirely predictable once I learned the actual rules.

console.log("5" + 3);   // "53" — the + operator prefers string concatenation if either operand is a string
console.log("5" - 3);   // 2   — the - operator only makes sense numerically, so it coerces "5" to a number
console.log("5" * "2"); // 10  — same reasoning, both coerced to numbers
console.log(true + 1);  // 2   — true coerces to 1
console.log("5" == 5);  // true  — == coerces types before comparing
console.log("5" === 5); // false — === compares type AND value, no coercion

I always use ===/!== over ==/!= specifically to avoid relying on these coercion rules, which are easy to misremember under pressure.

Explicit Type Conversion

console.log(Number("42"));    // 42
console.log(Number("abc"));   // NaN
console.log(String(42));      // "42"
console.log(Boolean(""));     // false
console.log(Boolean("false")); // true — any non-empty string is truthy, regardless of its content!
console.log(parseInt("42px")); // 42 — parses leading numeric characters, ignoring the rest
console.log(parseFloat("3.14 meters")); // 3.14

I always convert types explicitly (Number(), String(), Boolean()) rather than relying on implicit coercion, since explicit conversions make my intent obvious to anyone reading the code later.

Operators

Arithmetic Operators

console.log(10 + 3);  // 13
console.log(10 - 3);  // 7
console.log(10 * 3);  // 30
console.log(10 / 3);  // 3.3333333333333335
console.log(10 % 3);  // 1 — remainder (modulo)
console.log(2 ** 10); // 1024 — exponentiation

Assignment Operators

let total = 10;
total += 5;  // total = total + 5 -> 15
total -= 3;  // 12
total *= 2;  // 24
total /= 4;  // 6
total **= 2; // 36

Comparison Operators

console.log(5 > 3);   // true
console.log(5 >= 5);  // true
console.log(5 !== "5"); // true — strict inequality, checks type too

Logical Operators

console.log(true && false); // false
console.log(true || false); // true
console.log(!true);          // false

Nullish Coalescing and Optional Chaining

const settings = { theme: null };
console.log(settings.theme ?? "light"); // "light" — falls back only for null/undefined
console.log(settings.missing?.deeplyNested); // undefined — safely returns undefined instead of throwing

Logical Assignment Operators

let config = { retries: 0 };
config.retries ||= 3; // assigns only if current value is falsy -> becomes 3 (0 is falsy!)

let cache = { data: null };
cache.data ??= "default"; // assigns only if current value is null/undefined -> becomes "default"

Internal Working: How Type Coercion Actually Works

When JavaScript evaluates an operator like +, -, or == on operands of different types, it follows internal abstract operations defined in the ECMAScript spec, primarily ToPrimitive, ToNumber, and ToString. For + specifically, if either operand is a string (or converts to one via ToPrimitive), the engine performs string concatenation; otherwise, it converts both operands to numbers and adds them. This single rule explains nearly every +-related coercion surprise I’ve ever encountered.

console.log(1 + "1");     // "11" — number coerced to string
console.log([] + []);     // ""   — arrays convert to strings via ToPrimitive, both become "", concatenated
console.log([] + {});     // "[object Object]" — same mechanism, different string results
console.log(1 + {});      // "1[object Object]"

Objects define their own ToPrimitive behavior via valueOf() and toString() methods, which is why {} becomes "[object Object]" and arrays become their joined elements as a string.

Practical, Real-World Applications

Safely Parsing User Input

function parseAge(input) {
  const age = Number(input);
  if (Number.isNaN(age) || age < 0) {
    throw new Error("Invalid age");
  }
  return age;
}

Providing Configuration Defaults

function initApp(options = {}) {
  const config = {
    timeout: options.timeout ?? 5000,
    retries: options.retries ?? 3,
    debug: options.debug ?? false,
  };
  return config;
}

Best Practices

  • I use const by default, let when reassignment is genuinely needed, and never var.
  • I use ===/!== instead of ==/!= to avoid implicit coercion surprises.
  • I convert types explicitly with Number(), String(), Boolean() rather than relying on implicit coercion.
  • I use ?? instead of || when only null/undefined should trigger a fallback.

Common Mistakes to Avoid

  1. Relying on == for comparisons, leading to unexpected coercion-based bugs.
  2. Using || for defaults when 0 or "" are legitimate values.
  3. Forgetting NaN !== NaN, and using === NaN instead of Number.isNaN().
  4. Assuming const makes objects fully immutable, when it only prevents reassignment of the variable itself.

Debugging Tips

  • I use typeof and console.log liberally when I’m unsure what type a value actually is at runtime.
  • For unexpected string concatenation instead of addition, I check whether one operand is accidentally a string (common with values pulled from form inputs, which are always strings).
  • I use Number.isInteger() and Number.isFinite() for stricter numeric validation than the global isNaN()/isFinite(), which coerce their argument first.

FAQs

Q: Why does typeof null return "object"? A: It’s a long-standing bug from the very first JavaScript implementation in 1995 that was never fixed, since fixing it would break existing code across the web.

Q: What’s the difference between undefined and null? A: undefined means a variable has been declared but not assigned a value (or a function didn’t return anything); null is an explicit, intentional assignment representing “no value.”

Q: Are all objects passed by reference? A: The object reference itself is passed by value, but since it’s a reference, mutating the object through that reference affects the original object wherever else it’s referenced.

Q: When should I use BigInt instead of Number? A: When working with integers larger than Number.MAX_SAFE_INTEGER (about 9 quadrillion) or when exact large-integer precision is required, such as in cryptography or high-precision IDs.

Summary and Key Takeaways

  • Use const by default, let when reassignment is needed, and avoid var.
  • JavaScript has seven primitive types plus the object type; primitives compare by value, objects compare by reference.
  • Type coercion follows predictable internal rules (ToPrimitive, ToNumber, ToString) — understanding them removes most of JavaScript’s “weirdness.”
  • Use ===/!== and explicit conversions to avoid relying on implicit coercion.
  • ?? and ?. provide safer, more precise fallback and access patterns than || and manual checks.

Variables, types, and operators might seem like “the basics,” but genuinely understanding their internal behavior — not just memorizing syntax — is what let me write JavaScript that behaves the way I expect, every time.

References

Total
0
Shares

Leave a Reply

Previous Post
Understanding JavaScript's Event Loop

Understanding JavaScript’s Event Loop

Next Post
JavaScript Basics: Control Flow, Conditions and Loops

JavaScript Basics: Control Flow, Conditions and Loops

Related Posts