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
| Type | Example | typeof Result |
|---|---|---|
| String | "hello" | "string" |
| Number | 42, 3.14 | "number" |
| BigInt | 123n | "bigint" |
| Boolean | true, false | "boolean" |
| Undefined | undefined | "undefined" |
| Null | null | "object" (a famous, long-standing quirk) |
| Symbol | Symbol("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
constby default,letwhen reassignment is genuinely needed, and nevervar. - 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 onlynull/undefinedshould trigger a fallback.
Common Mistakes to Avoid
- Relying on
==for comparisons, leading to unexpected coercion-based bugs. - Using
||for defaults when0or""are legitimate values. - Forgetting
NaN !== NaN, and using=== NaNinstead ofNumber.isNaN(). - Assuming
constmakes objects fully immutable, when it only prevents reassignment of the variable itself.
Debugging Tips
- I use
typeofandconsole.logliberally 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()andNumber.isFinite()for stricter numeric validation than the globalisNaN()/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
constby default,letwhen reassignment is needed, and avoidvar. - 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.