JavaScript in one sitting
Welcome to JavaScript Deep Dive. This first lesson is a fast tour of the parts of JavaScript you use every day: values and types, variables, comparisons, conditions and loops. Each part comes with the trap that catches people, so the deeper lessons that follow have solid ground to stand on.
By the end you can
- Name the eight kinds of value in JavaScript and check one with
typeof - Choose between
letandconst, and say whyvaris out - Predict what
==,===,||and??return, including the surprising cases - Write and run a Node script in VS Code's terminal that summarises a store's daily sales
The everyday picture
On a new cashier's first day, the store manager does not hand over the whole operations manual. She gives a fifteen-minute briefing at the counter: this is the till, this is the scanner, this is how a card or mobile-wallet payment looks on screen. Then she adds the five mistakes every new cashier makes in the first week, such as scanning a multipack as one item, or giving change before the payment is confirmed.
This lesson is that briefing for JavaScript. If you have written some JavaScript before, most of it will be a reminder. If you are new, it gives you just enough to follow the rest of the course. Either way, the valuable part is the list of traps next to each topic, because those are what show up later as bugs in a sales report. All you need is Node installed and VS Code open (VS Code essentials covers the setup), and every example is a small grocery-store script you can run yourself.
Values and types: seven primitives and objects
Every value in JavaScript is one of eight types. Seven are primitives: single, unchangeable values. The eighth is object, which covers everything that holds other values: plain objects, arrays, functions, dates, Maps.
number: one type for all numbers, whole or fractional (1250,0.15). It is a 64-bit floating-point number, which is why0.1 + 0.2prints0.30000000000000004. Working with data explains why and what to do about money.bigint: whole numbers of any size, written with ann:9007199254740993n. Plain numbers stay exact only up to 9,007,199,254,740,991.string: text, in single quotes, double quotes or backticks.boolean:trueorfalse.undefined: "no value was ever given". A variable you declared but never set, or a property that does not exist.null: "deliberately empty". Your code or your API sets it on purpose.symbol: a unique id, mostly used inside libraries. You will rarely write one.
The typeof operator tells you the type as a string. It has one famous bug that can never be fixed, because fixing it would break old websites: typeof null is 'object'. Arrays also say 'object', so use Array.isArray(x) to check for an array.
console.log(typeof 10n, typeof Symbol('x'), typeof function () {}, typeof []);
console.log(typeof undefined, typeof 'a', typeof NaN, Array.isArray([]));
console.log(typeof null); // the old bugbigint symbol function object
undefined string number true
objectNotice typeof NaN is 'number'. NaN is the number you get when a conversion fails, for example Number('2 pcs').
Quick check
An API returns a list of order lines. What does typeof lines give you?
Arrays are objects, so typeof says 'object'. There is no 'array' type. To check for an array, use Array.isArray(lines).
typeof null wrongly says 'object'.Variables, operators and the two kinds of equal
Declare with const by default and with let when you will reassign the name, such as a running total. Both are block-scoped. Do not use var: it ignores blocks and has hoisting surprises that Functions, scope and closures shows in full. One more thing about const: it stops you from pointing the name at a new value, and it does not stop changes inside an object. const cart = [] followed by cart.push(item) is allowed. Objects, arrays and references is about exactly that.
Arithmetic operators are the usual + - * / % plus ** for powers. The trap is +: if either side is a string, it joins text instead of adding. Everything a user types into a form arrives as a string, so this bites often:
const qtyFromForm = '2'; // every form input value is a string
console.log(qtyFromForm + 1); // + with a string joins text
console.log(Number(qtyFromForm) + 1); // convert first, then add
console.log(Number(''), Number('2 pcs'));21
3
0 NaNNote that Number('') is 0, not an error. An empty quantity box becomes zero silently, so validate input before converting it.
For comparison, JavaScript has two equals. === (strict equality) is true only when both sides have the same type and the same value. == (loose equality) first converts the two sides towards a common type, a process called type coercion, and the rules are long and odd: 0 == '' is true, [] == false is true, null == 0 is false. Always use === and !==. The one idiom some teams allow is x == null, which is true for both null and undefined. ESLint's eqeqeq rule enforces this (VS Code for teams).
Quick check
A scanner reads barcode '0420019' and the database stores it as the string '0420019'. Which line should your code use to compare them?
=== compares type and value with no conversion, so it is the safe default. A single = is assignment, not comparison, and it would always be truthy here. == happens to work for two strings, but it invites coercion bugs the day one side becomes a number.
Truthy, falsy, and safe defaults
An if, a while, &&, || and ! all accept any value, not only booleans. They treat a value as true or false by a fixed rule. Exactly eight values are falsy: false, 0, -0, 0n, "", null, undefined and NaN. Every other value is truthy, including the string '0', the string 'false', an empty array and an empty object.
Quick check
A form sends a stock count. Which of these values makes if (count) run its body?
The string '0' is not empty, so it is truthy. The number 0, the empty string and null are all on the list of eight falsy values. This is also why if (count) is a poor check for "was a count entered": a real count of 0 fails it.
'0', [] and {}. The two highlighted values, null and undefined, are the only ones that ?? and ?. treat as missing.This matters most for defaults. a || b returns b whenever a is falsy, so a real zero gets replaced. The nullish coalescing operator a ?? b returns b only when a is null or undefined. For numbers such as a discount, a quantity or a stock level, where 0 is a real answer, use ??.
Optional chaining, a?.b, reads b only if a is not null or undefined, and gives undefined otherwise instead of crashing. The two work well together:
const discountCents = 0; // a real "no discount"
console.log(discountCents || 50, discountCents ?? 50);
const sale = { customer: null }; // a walk-in customer
console.log(sale.customer?.phone, sale.customer?.phone ?? 'walk-in');
console.log(sale.customer.phone); // no ?. here50 0
undefined walk-in
TypeError: Cannot read properties of null (reading 'phone')The last line stops the script; Node prints a stack trace, and the line above is the important part of it. Do not sprinkle ?. everywhere, though. If a sale must always have a store, sale.store?.siteId hides a real data bug behind a quiet undefined. Use ?. where "missing" is a normal, expected case.
Strings, conditions, loops and the console
Template literals use backticks and ${…} to put values inside text: `Store ${siteId}: ${lineCount} lines`. They can also span several lines. Prefer them to joining strings with +.
if / else if / else works as in any C-style language. switch compares with === and has one trap: without break or return, execution falls through into the next case. Sometimes that is what you want, so that several cases share one answer, such as the many mobile wallets a store accepts (M-Pesa, UPI, GCash and so on):
function channel(paidBy) {
switch (paidBy) {
case 'cash': return 'cash';
case 'mpesa': // no return: falls through on purpose
case 'upi':
case 'gcash': return 'mobile wallet';
case 'card': return 'card';
default: return 'other';
}
}
console.log(channel('upi'), channel('cheque'));mobile wallet otherFor loops, you need three forms. The classic for (let i = 0; i < n; i++) when you need the index. for (const sale of sales) to walk the values of an array, a string, a Map or a Set; this is the one to use most. And for (const key in obj), which walks the keys of an object. Do not use for…in on arrays: it gives you the indexes as strings ('0', '1') and can include extra properties. Inside any loop, continue skips to the next item and break stops. Working with data covers the array methods (map, filter, reduce) that replace many loops.
The console is your first debugging tool. console.log prints values (objects are shown with their contents), console.table prints an array or object as a table, and console.error writes to the error stream, which the terminal and log collectors treat separately. Errors, debugging and testing goes from here to real breakpoints.
Build it
Write a small Node script that summarises one store's day, run it in VS Code's terminal, then test your instincts with eight one-liners. If you want to practise before installing anything, the Practice section below runs two exercises in your browser.
-
Open a folder and a terminal
Make a folder, open it in VS Code with
code .(VS Code essentials), and open the integrated terminal with Ctrl+` on both Windows and macOS, or the Command Palette command "View: Toggle Terminal". These commands are the same in PowerShell, Git Bash and zsh. Any Node 22 or 24 version is fine.Terminalmkdir js-basics cd js-basics code . node -vOutputv22.22.0 -
Write the script
Create
daily-sales.js. It skips voided lines, totals in cents, groups the money by payment method and remembers the biggest line. Every idea from this lesson is in it:constandlet, a truthiness check onvoided,??for a missing key,?.for a possibly empty result, template literals andfor…of. The last few lines format cents as US dollars withIntl.NumberFormat, which Working with data explains.JavaScript// daily-sales.js: summarise one store's sales for one day const siteId = 'S014'; const sales = [ // prices in cents: 1250 cents = $12.50 { id: 1, articleCode: '100234', qty: 2, unitPrice: 1250, paidBy: 'cash' }, { id: 2, articleCode: '200871', qty: 1, unitPrice: 3999, paidBy: 'wallet' }, { id: 3, articleCode: '100234', qty: 3, unitPrice: 1250, paidBy: 'card' }, { id: 4, articleCode: '310045', qty: 1, unitPrice: 450, paidBy: 'cash', voided: true }, { id: 5, articleCode: '420019', qty: 6, unitPrice: 199, paidBy: 'wallet' }, ]; let totalCents = 0; let lineCount = 0; let biggest = null; // the largest sale line so far const byMethod = {}; // e.g. { cash: 2500, wallet: 3999 } for (const sale of sales) { if (sale.voided) continue; // skip cancelled lines (undefined is falsy) const lineTotal = sale.qty * sale.unitPrice; totalCents += lineTotal; lineCount += 1; byMethod[sale.paidBy] = (byMethod[sale.paidBy] ?? 0) + lineTotal; if (biggest === null || lineTotal > biggest.lineTotal) { biggest = { articleCode: sale.articleCode, lineTotal }; } } const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }); const dollars = (cents) => usd.format(cents / 100); // for display only console.log(`Store ${siteId}: ${lineCount} lines, total ${dollars(totalCents)}`); for (const [method, cents] of Object.entries(byMethod)) { console.log(` ${method.padEnd(6)} ${dollars(cents).padStart(8)}`); } console.log(`Biggest line: article ${biggest?.articleCode ?? 'none'}`); -
Run it, then run it on every save
Run it once. Then try
node --watch, which re-runs the script each time you save (stable since Node 22). Change a quantity, press Ctrl+S (Cmd+S on macOS) and watch the total change. Stop it with Ctrl+C.Terminalnode daily-sales.js node --watch daily-sales.jsOutputStore S014: 4 lines, total $114.43 cash $25.00 wallet $51.93 card $37.50 Biggest line: article 200871The voided $4.50 line is not in the cash total, and
byMethodcreated each key the first time it saw it, thanks to?? 0. -
Predict the output: eight one-liners
Save this as
predict.js. Before you run it, write down what you expect each line to print. Then runnode predict.jsand compare.JavaScriptconst order = { store: null }; // setup console.log(typeof null); // 1 console.log('5' + 3, '5' - 3); // 2 console.log(0 == '', 0 === ''); // 3 console.log(null == undefined, null === undefined); // 4 console.log(0 || 50, 0 ?? 50); // 5 console.log(Boolean('0'), Boolean(''), Boolean([])); // 6 console.log(NaN === NaN, Number.isNaN(Number('12a'))); // 7 console.log(order.store?.siteId, order.items?.length ?? 0); // 8Show the real output and why
Outputobject 53 2 true false true false 50 0 true false true false true undefined 0- The old
typeof nullbug. +with a string joins text;-only works on numbers, so it converts'5'to 5.==converts''to 0, so they are "equal";===sees two types.nullandundefinedare loosely equal to each other and to nothing else.||replaces any falsy value;??keeps the real zero.- A non-empty string is truthy even if it says
'0'; an empty array is truthy too. NaNis not equal to itself. UseNumber.isNaNto test for it.order.storeisnull, so?.stops and givesundefined;itemsdoes not exist, so?? 0supplies the default.
- The old
Bring it to your team
These small rules show up as real bugs in money and stock code. Ask about them early.
- "Do we keep every price and total as integer cents, and where exactly do we turn it into currency text?"
- "Is ESLint's
eqeqeqrule on, along withno-varandprefer-const?" - "For this default value, can zero be a real answer? If yes, are we using
??instead of||?" - "When a field is missing in this API response, is it
null, absent, or an empty string? Which one does the app check for?" - "Where do we convert form input to numbers, and what happens with an empty box or '2 pcs'?"
Red flags
==comparisons in pricing, stock or payment code.qty || 1,discount || DEFAULTorstock || 0on values where 0 is valid.- Prices stored as
12.5in some places and1250in others. typeof x === 'object'used as an "is this an object" check without also ruling outnull.
Practice
Fix two input bugs from the Partner app
Both functions below have a bug from this lesson. addQty receives the quantity exactly as the form sends it, as text. It must return a number, and it must throw an Error for input that is not a whole number of at least 1, such as an empty box or '2 pcs'. discountFor must keep the line's own discount, including a real 0, and use the 50-cent default only when the discount is missing (null or undefined). Press Run to check your answer.
// 1. The form sends qty as text, such as '2'. Return the new quantity as a number.
function addQty(currentQty, qtyFromForm) {
return currentQty + qtyFromForm;
}
// 2. Keep the line's own discount. Use the default only when it is missing.
const DEFAULT_DISCOUNT_CENTS = 50;
function discountFor(line) {
return line.discountCents || DEFAULT_DISCOUNT_CENTS;
}
test("adds '2' as a number: 1 + '2' must be 3, not the text '12'", () => {
expect(addQty(1, '2')).toBe(3);
});
test("rejects an empty box and '2 pcs' with an Error (Number('') is 0 and Number('2 pcs') is NaN)", () => {
expect(() => addQty(1, '')).toThrow();
expect(() => addQty(1, '2 pcs')).toThrow();
});
test('keeps a real discount of 0 (|| treats 0 as missing, ?? does not)', () => {
expect(discountFor({ discountCents: 0 })).toBe(0);
});
test('uses the 50-cent default only when the discount is null or undefined', () => {
expect(discountFor({})).toBe(50);
expect(discountFor({ discountCents: null })).toBe(50);
});
test('keeps a discount the shop set, such as 120 cents', () => {
expect(discountFor({ discountCents: 120 })).toBe(120);
});
Show a solution
function addQty(currentQty, qtyFromForm) {
const qty = Number(qtyFromForm);
// Number('') is 0 and Number('2 pcs') is NaN, so check before using it
if (qtyFromForm.trim() === '' || !Number.isInteger(qty) || qty < 1) {
throw new Error(`bad quantity: '${qtyFromForm}'`);
}
return currentQty + qty;
}
const DEFAULT_DISCOUNT_CENTS = 50;
function discountFor(line) {
return line.discountCents ?? DEFAULT_DISCOUNT_CENTS; // only null or undefined
}Total a day's sales by payment method
Write totalsByMethod(sales). Each sale looks like { qty: 2, unitPrice: 1250, paidBy: 'cash', voided: true }, and voided may be missing. Return an object with the total in cents per payment method, skipping voided sales, such as { cash: 2500, card: 3750 }. The starter has three bugs from this lesson.
function totalsByMethod(sales) {
const byMethod = {};
for (const sale in sales) {
byMethod[sale.paidBy] += sale.qty * sale.unitPrice;
}
return byMethod;
}
const day = [
{ qty: 2, unitPrice: 1250, paidBy: 'cash' },
{ qty: 1, unitPrice: 3999, paidBy: 'wallet' },
{ qty: 3, unitPrice: 1250, paidBy: 'card' },
{ qty: 1, unitPrice: 450, paidBy: 'cash', voided: true },
{ qty: 6, unitPrice: 199, paidBy: 'wallet' },
];
test('walks the sale objects themselves (for...in gives the indexes "0", "1"; use for...of)', () => {
expect(Object.keys(totalsByMethod(day)).sort()).toEqual(['card', 'cash', 'wallet']);
});
test('starts each method at 0 (undefined + 2500 is NaN; use ?? 0)', () => {
expect(totalsByMethod([{ qty: 2, unitPrice: 1250, paidBy: 'cash' }])).toEqual({ cash: 2500 });
});
test('skips voided sales and adds up the rest in cents', () => {
expect(totalsByMethod(day)).toEqual({ cash: 2500, wallet: 5193, card: 3750 });
});
test('returns an empty object for a day with no sales', () => {
expect(totalsByMethod([])).toEqual({});
});
Show a solution
function totalsByMethod(sales) {
const byMethod = {};
for (const sale of sales) { // of: the values, not the indexes
if (sale.voided) continue; // a missing voided is undefined, so falsy
const line = sale.qty * sale.unitPrice;
byMethod[sale.paidBy] = (byMethod[sale.paidBy] ?? 0) + line;
}
return byMethod;
}Check yourself
A partner shop sets an article's discount to 0 on purpose. The code reads const d = line.discountCents || 50;. What is d, and what should the code use?
0 is falsy, so || falls back to 50 and the shop gets a discount it never set. ?? keeps any value that is not null or undefined, so a real zero stays zero. === is about comparison, not defaults.
A quantity comes from a form as '2'. The code computes qty + 1. What does it give?
With +, a string on either side wins and you get text. Other operators like - and * do convert to numbers, which is why '5' - 3 is 2. Convert with Number(qty) first, and validate, because Number('') is 0.
What does const cart = []; cart.push('100234'); do?
const means the name cart always points to the same array. The array itself can still change. Arrays are objects, not primitives. To stop changes you need Object.freeze or copies, covered in Objects, arrays and references.
Flashcards
Key takeaways
- Seven primitive types plus objects.
typeof nullis'object'; check arrays withArray.isArray. - Use
constby default,letwhen you reassign, nevervar.constdoes not freeze objects. - Always compare with
===. Watch+with strings, and convert form input withNumber()after validating it. - Only eight values are falsy. Use
??for defaults when 0 or''is a real value, and?.where "missing" is normal. - Keep money in integer cents and format it only for display.
Sources
- JavaScript data types and data structures, MDN Web Docs
- Equality comparisons and sameness, MDN Web Docs
- Nullish coalescing operator (??), MDN Web Docs