null and undefined, and when each shows up;typeof, including its one famous bug;const are familiar.Learn JS Series):Before we open today's topic, here are worked solutions to the three exercises from last time. Read them next to your own attempts, the small differences are usually where the real understanding lives.
Exercise 1 - a const array you can still push to:
const words = ["ownership", "closure", "prototype"];
words.push("hoisting");
console.log(words); // ["ownership", "closure", "prototype", "hoisting"]
The insight: const glues the name words to one specific array object. .push() changes what is inside that array, it does not point words at a different array, so const is perfectly happy. This is exactly the binding-versus-value distinction we hammered on in episode two.
Exercise 2 - triggering the Temporal Dead Zone on purpose:
// console.log(tooEarly); // ERROR (runtime): Cannot access 'tooEarly' before initialization
const tooEarly = "finally allowed";
console.log(tooEarly);
The insight: the variable tooEarly exists from the top of the block, but it is unusable until its declaration line runs. That is the TDZ turning a mistake into a loud error, in stead of a silent undefined.
Exercise 3 - fixing the loop so total becomes 6:
let total = 0; // changed const -> let
for (const n of [1, 2, 3]) {
total = total + n;
}
console.log(total); // 6
The insight: total genuinely needs to be reassigned on each pass, so it must be let, not const. Note that n can stay const because it is a fresh binding each iteration, a detail we saw when we looked at loops and let.
Right, today we go one level deeper. We have been declaring variables and putting values into them, but we have been vague about what a "value" actually is. Time to fix that, because the answer is the bedrock everything else is built on.
In JavaScript, every single value is one of two broad categories: a primitive, or an object. That is the whole universe. There are exactly seven primitive types, and today is about all seven of them. Objects (which include arrays and functions, as we already glimpsed) get their own episodes later, once we have the primitives solid.
The seven primitives are: number, string, boolean, undefined, null, symbol, and bigint. Memorise that list, because "is this thing a primitive or an object?" is a question you will ask yourself constantly, often without realising it, every time you reason about how a value behaves.
The defining feature of a primitive is that it is immutable and compared by value. You cannot change the number 5 into something else, 5 is just 5, forever. You cannot reach inside the string "hello" and edit one letter in place. Primitives are the atoms of the language: fixed, indivisible little facts. Objects, by contrast, are mutable and compared by reference, which is a very different world with very different rules. We will feel the full weight of that contrast later, but keep it in the back of your mind as we meet each type.
Most languages you may have heard of have separate types for integers and decimals: int, float, double, long, and friends. JavaScript has exactly one: number. Every number you write, whole or fractional, positive or negative, tiny or astronomically large, is the same 64-bit floating point value under the hood (specifically the IEEE 754 double, the same format C calls double).
const whole = 42;
const negative = -7;
const decimal = 3.14;
const big = 1_000_000; // underscores are just visual separators
console.log(whole, negative, decimal, big);
Those underscores in 1_000_000 are a nice touch: they are purely for human eyes, the engine ignores them completely, so you can group digits the way you would with thousands separators. 1_000_000 and 1000000 are the identical value.
This one-type simplicity is genuinely convenient, you never have to think about integer overflow or picking the right width like you do in C or Rust. But it has a price, and it is worth seeing the famous consequence right now even though we give numbers a whole dedicated episode soon:
console.log(0.1 + 0.2); // 0.30000000000000004, not 0.3
console.log(0.1 + 0.2 === 0.3); // false
The first time you see that, it looks like a bug in the language. It is not. It is how binary floating point works in every language that uses IEEE 754, which is nearly all of them: Python, Java, C, Ruby, they all print the same surprise. The short version is that 0.1 and 0.2 cannot be represented exactly in binary, any more than 1/3 can be written exactly in decimal, so the tiny rounding errors leak out when you add them. We will learn exactly why, and the practical tricks for coping with it, in the numbers episode.
The number type also includes three special values that will eventually surprise you, so meet them early: Infinity, -Infinity, and NaN (Not a Number).
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(typeof NaN); // "number" -- yes, "Not a Number" IS a number
That last line is a classic head-scratcher: NaN is literally called "Not a Number", yet typeof NaN reports "number". The name describes what it means (the result of a nonsensical calculation), not what type it is. NaN is a number that represents an invalid numeric result, and it has some genuinely weird properties (it is the only value in the language not equal to itself) that we will pull apart later.
A string is a sequence of characters, JavaScript's type for text. The language accepts single quotes, double quotes, and backticks, and for plain text they mostly behave the same:
const single = 'hello';
const double = "hello";
const backtick = `hello`;
console.log(single === double); // true - same string value
console.log(single === backtick); // true - still the same value
Single and double quotes are fully interchangeable: pick one and be consistent (most codebases settle on one by convention and let the linter enforce it). The only practical reason to prefer one is to avoid escaping: if your text contains a ', wrap it in "..." so you do not have to backslash it, and vice versa.
Backticks are the special one. They are template literals, and they give you two superpowers that ordinary quotes do not have: interpolation and multi-line text.
const name = "scipio";
const episode = 3;
const greeting = `Dear ${name}, welcome to episode ${episode}.`;
console.log(greeting); // Dear scipio, welcome to episode 3.
Anything inside ${...} is evaluated as a real JavaScript expression and dropped into the string, so you never have to glue pieces together with + and fuss over spaces. And because template literals can span multiple lines directly in the source, they are perfect for anything with structure:
const name = "scipio";
const multiline = `Dear ${name},
welcome to episode three.
Enjoy the primitives.`;
console.log(multiline);
One crucial fact to file away now: strings are immutable. Any method that seems to change a string actually returns a brand new string and leaves the original completely untouched.
const original = "hello";
const shouted = original.toUpperCase();
console.log(shouted); // "HELLO" -- a new string
console.log(original); // "hello" -- the original is unchanged
toUpperCase() did not edit original in place, it could not, strings are immutable. It computed a new string and handed it back. That immutability is exactly what makes strings primitives, and we will give strings a full episode of their own shortly, because there is a lot hiding in "just text" (Unicode, code points, and the methods you will actually reach for daily).
The boolean type is the simplest of the lot: it has exactly two values, true and false. They are the result of every comparison and the fuel for every if:
const isReady = true;
const isBigger = 10 > 3; // true
const isEqual = 5 === "5"; // false - different types, and === does NOT convert
console.log(isReady, isBigger, isEqual);
Notice 5 === "5" is false: the number 5 and the string "5" are different types, and the strict-equality operator === refuses to pretend otherwise. That refusal is a feature, and we will devote a whole episode to equality and coercion later, because the loose == operator does convert types and causes real confusion. For now the rule is simple: prefer ===.
Booleans look almost too small to matter, but nearly all control flow in any program comes down to producing a boolean and then reacting to it. Every branch, every loop condition, every guard clause is, underneath, a question that answers true or false.
Here is a genuine rite of passage. JavaScript has two values that both mean "no value", and understanding the difference between them is one of those small things that separates people who are fighting the language from people who are fluent in it.
undefined is what JavaScript hands you when something has simply not been given a value. A variable that is declared but not assigned, a missing object property, a function that returns nothing, all evaluate to undefined. Think of it as the system saying "there is nothing here, and nobody put anything here".
let notYetSet;
console.log(notYetSet); // undefined -- declared, never assigned
const person = { name: "scipio" };
console.log(person.age); // undefined -- no such property exists
function doesNothing() {}
console.log(doesNothing()); // undefined -- no return statement
null, by contrast, is a value that you assign deliberately to mean "intentionally empty". It is the programmer stepping in and saying "this is empty on purpose, I decided that", as opposed to the system's accidental, nobody-was-here undefined.
let selectedItem = null; // I am explicitly stating: nothing is selected yet
console.log(selectedItem); // null
The rule of thumb that keeps me honest: undefined is the language's default absence, null is your intentional absence. When you want to signal "this slot is empty, and I meant it", assign null. When you have not touched something yet, let it be undefined. Getting into this habit early makes your code communicate its own intentions, which is a theme you will hear from me a lot.
There is a good practical reason JavaScript gives you a modern tool for exactly this pair, and here is a taste of it:
const config = { timeout: null }; // explicitly "no timeout set"
const timeout = config.timeout ?? 5000; // ?? provides a fallback for null/undefined
console.log(timeout); // 5000
That ?? is the nullish coalescing operator, and it treats null and undefined (the two "nothing" values) as the cases that need a fallback, while leaving 0 or "" alone. We meet it properly in a later episode, but it exists precisely because these two absence-values are so central.
Two of the seven primitives arrived in modern JavaScript, well after the original five. As a beginner you can treat them lightly for now, but you absolutely should know they exist and roughly what they are for, so nothing surprises you later.
A symbol is a guaranteed-unique value. Its main job is to serve as a special object key that can never accidentally collide with another key. The important, slightly magical property is that every symbol you create is unique, even two made with the identical description:
const id1 = Symbol("id");
const id2 = Symbol("id");
console.log(id1 === id2); // false - always unique, despite same description
console.log(id1.description); // "id" - the description is just a label for humans
That uniqueness is the whole point: if a library uses a symbol as a key on your object, it is physically impossible for your own code to clash with it by accident. Symbols power some deep language machinery (like making an object iterable) that we will explore much later.
A bigint is for integers larger than a regular number can safely hold. Remember that all numbers are 64-bit floats, which means they lose precision above about 9 quadrillion (2 to the 53rd, to be exact). When you truly need exact whole-number arithmetic beyond that, bigint has no upper limit. You make one by appending n:
const huge = 9007199254740993n; // the n suffix makes it a bigint
console.log(huge + 1n); // 9007199254740994n - exact
console.log(typeof huge); // "bigint"
You cannot casually mix bigint and number in the same arithmetic (JavaScript throws rather than silently guess), which is a sign of how deliberately separate they are. Both symbols and bigints get proper episodes later. For today, just recognise them on sight.
The typeof operator is your everyday tool for asking "what kind of value is this?". It takes any value and returns a string naming its type:
console.log(typeof 42); // "number"
console.log(typeof "hi"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol()); // "symbol"
console.log(typeof 10n); // "bigint"
All correct and sensible. But there is one wart, a bug so ancient it can never be fixed because too much of the web now depends on the broken behaviour:
console.log(typeof null); // "object" - this is WRONG, but permanent
typeof null returns "object", even though null is very much a primitive, not an object. This is a genuine bug from the very first version of JavaScript in 1995, baked into the way values were tagged in memory. Fixing it was proposed and rejected, because it would silently break an enormous amount of existing code that has learned to work around it. So it stays, forever. The practical lesson: when you need to check specifically for null, do not trust typeof, compare directly with === null in stead.
const value = null;
if (value === null) {
console.log("it really is null"); // the reliable way to check
}
I claimed at the top that primitives are compared by value, and I want to make that concrete before we finish, because it is the single most useful thing to understand about them. When you copy a primitive, you get a completely independent copy. Changing one has no effect on the other, they were never connected in the first place:
let a = 10;
let b = a; // b gets its OWN copy of the value 10
b = 99; // changing b...
console.log(a); // 10 -- ...leaves a completely untouched
console.log(b); // 99
This feels obvious for a number, and it should. But it is precisely the behaviour that objects do not have, and the contrast is where beginners get bitten. Here is the same shape of code with an object, just so you can see the difference coming:
const one = { count: 10 };
const two = one; // two points at the SAME object, not a copy
two.count = 99; // mutate through two...
console.log(one.count); // 99 -- ...and one sees it too, because they share
With the primitive, a and b were two separate values. With the object, one and two are two names for one shared thing. That is the by-value versus by-reference split, and it is arguably the most important single idea in the whole language. We are only previewing it today, we get a full, careful episode on it soon, but every primitive you met above lives on the clean, simple, by-value side of that line, and that is a big part of why they are pleasant to reason about.
Many of you arrived here from the Learn Python Series, so a quick look sideways sharpens the picture of what JavaScript actually chose, because every language draws these lines a little differently.
Python also has a small set of built-in scalar types, but it splits numbers where JavaScript unifies them. Python has a true arbitrary-precision int and a separate float, plus a bool, str, and its own "nothing" value None (a single one, where JavaScript has two):
# Python splits what JavaScript merges into one `number`.
x = 42 # int, arbitrary precision built in
y = 3.14 # float, a separate type
nothing = None # Python has ONE null-like value, not two
So Python's int never loses precision the way a JavaScript number does past 9 quadrillion, that is the job JavaScript hands to the newer bigint. And Python has a single None, sidestepping the whole null-versus-undefined distinction that trips up JS newcomers.
Rust and C go the opposite direction entirely: they make you choose the exact size and kind of every number at compile time, because they care deeply about memory layout and performance:
// Rust: you pick the exact numeric type, and immutability is the default.
let a: i64 = 42; // a 64-bit signed integer
let b: f64 = 3.14; // a 64-bit float, chosen explicitly
// there is no null at all -- absence is modelled with Option
That is a completely different philosophy: where JavaScript says "one number type, do not worry about it", Rust says "state exactly what you mean, and I will make it fast and safe". Rust also famously has no null, it models "maybe absent" with a type called Option, precisely to avoid the class of bugs that null invites. Go sits closer to Rust on numbers (it has int, float64, bool, string, and a nil for absence) but is less strict about it than Rust.
The takeaway is not that any of these is "right". It is that JavaScript optimised for ease of getting started: one number type, and text and booleans that just work, at the cost of some precision surprises and a doubled-up idea of "nothing". Knowing where JS sits on that spectrum helps you understand why it behaves the way it does, and it makes moving between languages later feel like adjusting a dial rather than learning from scratch.
Let us close with a tiny program that uses several primitives at once, so you can see them cooperating in something slightly more real than a one-liner:
const userName = "ada"; // string
const age = 36; // number
const isAdmin = false; // boolean
let lastLogin = null; // null -- explicitly "never logged in yet"
const sessionId = Symbol("session"); // symbol -- a guaranteed-unique key
const summary = `${userName} (age ${age}) admin=${isAdmin} lastLogin=${lastLogin}`;
console.log(summary); // ada (age 36) admin=false lastLogin=null
console.log(typeof sessionId); // "symbol"
lastLogin = "2026-08-16"; // now we assign a real value
console.log(lastLogin ?? "never"); // "2026-08-16" -- ?? only falls back on null/undefined
Look at each choice. userName is a string, age is a number, isAdmin is a boolean. lastLogin starts as null because we are deliberately saying "no login has happened", not leaving it vaguely undefined. sessionId is a symbol because we want a key that can never collide. And when we interpolate them into a template literal, JavaScript converts each to a readable form for display. Every value in that little program is a primitive, immutable and compared by value, and that is precisely why it is so easy to reason about what it does.
typeof of each of these: 100, "text", false, undefined, a Symbol(), 42n, and null. Which one lies to you, and what does it falsely claim to be?const car = { brand: "generic" } and print car.brand and car.speed. Explain in one sentence why one is a real value and the other is undefined.null or undefined is the more honest choice, assign it, then use the ?? operator to print the string "guest" as a fallback. Add a one-line comment explaining your reasoning.number is a single 64-bit float for all math, which is why 0.1 + 0.2 is not exactly 0.3, and why NaN, Infinity, and -Infinity live inside it.string comes in single, double, and backtick quotes; backticks are template literals with ${...} interpolation and multi-line text, and strings are immutable.boolean is just true/false, the output of every comparison and the fuel for every branch.undefined is the system's "not set"; null is your intentional "empty on purpose", and ?? is built to handle exactly those two.symbol gives guaranteed-unique keys and bigint gives arbitrary-size integers; both are newer and get their own episodes.typeof inspects a value's type, but famously (and permanently) reports typeof null as "object", so check for null with === null.Next episode we put these values to work with operators and expressions: arithmetic, comparison, the logical operators, and the surprisingly useful trick of short-circuiting.
Thanks for reading, and see you in the next one.