JavaScript Foundations for TypeScript and React
Build a broad, working refresher of the core JavaScript concepts — from what a program actually is to closures, async/await, and the DOM — so you have everything you need to start learning TypeScript and React comfortably.
TypeScript is built entirely on top of JavaScript, and React is written in TypeScript/JavaScript, so before touching either one it pays to make sure the JavaScript fundamentals underneath them are solid; this lesson is a fast, example-driven tour of that foundation, not a replacement for EngiLipi's full JavaScript course.
From Source Code to a Running Program
Programming is the act of writing precise, unambiguous instructions — source code — that a computer can carry out. Source code by itself does nothing; it has to be turned into something the machine can execute, and there are two classic strategies for doing that. A compiler reads the entire source file up front, checks it, and translates it into another form (often machine code or another language) before anything runs. An interpreter instead reads and executes code line by line, as it goes, without a separate translation pass. Many modern language runtimes, including the ones that run JavaScript, blend both ideas: they parse and partially compile code just-in-time for speed while still behaving like an interpreter from the outside.
JavaScript is the language that runs natively in every web browser and, via Node.js, on servers and command lines too. A JavaScript runtime is the environment that actually executes your code — the browser's engine (like V8 in Chrome) or Node.js — and it provides not just the language itself but surrounding APIs like the DOM, fetch, timers, and the file system.
TypeScript is not a separate language that competes with JavaScript — it is JavaScript with an added static type-checking layer on top. You write TypeScript, a compiler (tsc) checks it for type mistakes, and then strips the types away to produce plain JavaScript that runs in any JavaScript runtime. The rest of this lesson refreshes the JavaScript you'll be writing underneath those types.
• Programming = writing source code (instructions) for a computer to execute. • Compiler = translates all the code up front, before execution (e.g. tsc, C++ compilers). • Interpreter = reads and runs code line-by-line as it goes. • Runtime = the environment executing the code (browser engine, Node.js). • JavaScript = the language that runs in browsers and on Node.js. • TypeScript = JavaScript + static types, compiled down to plain JavaScript.
Variables, Values, and Types
JavaScript gives you three keywords for declaring variables: var, let, and const. var is the legacy way — it is function-scoped (not block-scoped), gets hoisted with a confusing default value of undefined, and can be silently redeclared, which causes real bugs. Modern JavaScript avoids it almost entirely. let declares a block-scoped variable that can be reassigned, and const declares a block-scoped binding that cannot be reassigned after its initial value — though if that value is an object or array, its contents can still be mutated. The modern default is: reach for const first, use let only when you know the variable must change, and treat var as something you recognize in old code but never write yourself.
JavaScript's primitive types are string, number, boolean, null, undefined, bigint, and symbol — these hold single, immutable values. Everything else is an object: plain objects ({ key: value } collections), arrays ([1, 2, 3], ordered lists), functions, dates, maps, sets, and more. Objects are reference types — when you copy a variable holding an object, you copy the reference to the same underlying data, not a fresh independent copy.
Destructuring lets you unpack values out of arrays or objects into individual variables in one expression, and the spread/rest syntax (...) either expands an iterable into individual elements (spread) or gathers multiple elements into one array/object (rest) depending on where it's used. Both are used constantly in modern JavaScript and React code.
• const point = { x: 1, y: 2 }; point.x = 5; // allowed — the object's contents changed, the binding didn't. • const [first, second] = [10, 20]; — array destructuring. • const { x, y } = point; — object destructuring. • const combined = [...arrA, ...arrB]; — spread to merge arrays. • function sum(...nums) { } — rest parameters gather remaining arguments into an array.
Operators, Conditions, and Loops
JavaScript's operators cover arithmetic (+ - * / % **), comparison (< > <= >=), equality (===/!== for strict, type-aware comparison, and the legacy ==/!= which perform type coercion and should generally be avoided), logical operators (&& || !), and two newer, very useful operators: optional chaining (?.), which safely accesses a nested property and short-circuits to undefined if something along the chain is null/undefined instead of throwing, and nullish coalescing (??), which supplies a fallback value only when the left side is null or undefined (unlike ||, which also falls back on falsy values like 0 or "").
Branching logic is written with if/else if/else for general conditions, and switch when you're comparing one value against several fixed possibilities. Looping comes in several flavors: the classic for (let i = 0; i < n; i++) counts through a range; while and do...while repeat based on a condition (the latter always runs at least once); for...of iterates the *values* of an iterable like an array or string; and for...in iterates the *enumerable keys* of an object (and is best avoided on arrays, where for...of or array methods are the modern choice).
• user?.address?.city — optional chaining avoids a crash if user or address is null/undefined. • const name = input ?? "Guest"; — nullish coalescing only falls back on null/undefined, not 0 or "". • for (const item of [1,2,3]) — iterates values. • for (const key in { a: 1, b: 2 }) — iterates keys.
Functions, Scope, and Closures
Functions are JavaScript's core unit of reusable behavior. They can be declared with the function keyword, as expressions, or — most common in modern code — as arrow functions ((a, b) => a + b), which are more compact and, crucially, do not have their own this binding (they inherit this from the enclosing scope, which is exactly what you usually want). Functions accept parameters, can give parameters default values (function greet(name = "Guest")), and can gather extra arguments with rest parameters (...args). A function that takes another function as an argument, or returns one, is called a higher-order function — array.map(callback) is a everyday example, where callback is passed in as an argument.
Scope determines where a variable is visible. JavaScript has function scope (for var) and block scope (for let/const — visible only inside the { } they're declared in). A closure happens when an inner function 'remembers' the variables from the outer function it was created in, even after the outer function has finished running — this is how you build things like counters, memoized functions, and private state in JavaScript.
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during compilation — function declarations are hoisted fully (usable before they appear in the code), var declarations are hoisted but initialized to undefined, and let/const are hoisted into a 'temporal dead zone' where referencing them before their declaration throws an error. Finally, this refers to the context a function is called in — it depends on *how* a function is invoked, not where it's defined, except for arrow functions, which lock in the this of their surrounding scope.
• function makeCounter() { let count = 0; return () => ++count; } — closure keeps count alive. • const add = (a, b) => a + b; — arrow function, no own this. • function log(...args) { console.log(args); } — rest parameters. • array.map(x => x * 2) — map is a higher-order function taking a callback.
Asynchronous JavaScript and the Event Loop
JavaScript runs on a single thread, but it handles slow operations (network requests, timers, file reads) without freezing everything else by using an event loop. A Promise represents a value that isn't available yet but will resolve (succeed) or reject (fail) in the future. async/await is syntax built on top of promises that lets you write asynchronous code that reads like ordinary synchronous code — an async function always returns a promise, and await pauses that function (without blocking the whole program) until the awaited promise settles.
Under the hood, the event loop coordinates three things: the call stack (currently running code), the microtask queue (promise callbacks, which run before the next render and before any macrotask), and the macrotask/task queue (things like setTimeout callbacks, UI events, and I/O, which run one at a time between renders). Understanding that microtasks always drain before the next macrotask explains a lot of ordering surprises in async code.
fetch is the standard browser (and Node) API for making network requests, and it returns a promise — a typical pattern is const data = await (await fetch(url)).json(); inside an async function, wrapped in try/catch to handle failures.
• Promise states: pending → fulfilled or rejected. • async function load() { const res = await fetch(url); return res.json(); } • Microtasks (promise .then/await continuations) run before macrotasks (setTimeout, events). • try { await risky(); } catch (err) { /* handle rejection */ }
Modules, Classes, and the DOM
Modern JavaScript organizes code into modules — separate files that explicitly export values and import them elsewhere, replacing the old approach of everything living in global scope. Classes provide a familiar, structured syntax (class, constructor, methods, extends for inheritance) for creating objects that share behavior; under the hood, JavaScript classes are still built on prototypes — every object has a hidden link to a prototype object it inherits methods and properties from, and classes are essentially a cleaner syntax over that same prototype chain.
In the browser, the DOM (Document Object Model) is the tree of objects representing the page's HTML, and JavaScript can read and modify it (document.querySelector, element.textContent, adding event listeners, etc.) to make pages interactive. React, which you'll meet later in this course, is essentially a structured, component-based way of describing what the DOM should look like, letting a library manage the actual DOM updates for you — which is exactly why a strong grip on plain JavaScript functions, closures, and async code pays off immediately once you start writing React with TypeScript.
• export function helper() {} / import { helper } from "./utils"; • class Animal { constructor(name) { this.name = name; } speak() { return ${this.name} makes a sound; } } • class Dog extends Animal { speak() { return ${this.name} barks; } } — inheritance via prototypes. • document.querySelector("#app").textContent = "Hello"; — direct DOM manipulation.
A quick tour in one snippet
const/let are the modern defaults — var is legacy and should be avoided in new code. Arrow functions are typically preferred for callbacks because they don't rebind this. async functions always return a Promise, even if you return a plain value inside them.
// modern variables
const PI = 3.14159;
let total = 0;
// destructuring + defaults + rest
function summarize({ name, age = 18 }, ...tags) {
return `${name} (${age}): ${tags.join(", ")}`;
}
// arrow function + closure
function makeCounter() {
let count = 0;
return () => ++count;
}
const next = makeCounter();
next(); next(); // count is now privately 2
// async/await
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}Code Examples
makeMultiplier is a higher-order function because it returns another function. That returned function is a closure — it keeps a private reference to factor from its enclosing scope, so double and triple each remember their own separate value of factor even after makeMultiplier has finished running. Expected result: 10 15 return function (n) { return n * factor; }; — this inner function closes over factor.
The function awaits the network response, then safely reads a deeply nested property with optional chaining (?.) so it won't throw if address is missing, and falls back to a default string with nullish coalescing (??) only if the result is null or undefined — not if it happens to be an empty string.
Common mistake: Using user.address.city || "Unknown city" instead of ?? would incorrectly replace a legitimately empty string "" with the fallback — || reacts to any falsy value, ?? only to null/undefined.
function makeMultiplier(factor) {
return function (n) {
return n * factor;
};
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15async function getCityName(userId) {
try {
const res = await fetch(`/api/users/${userId}`);
const user = await res.json();
return user?.address?.city ?? "Unknown city";
} catch (err) {
return "Error loading city";
}
}The event loop, simplified
After each piece of synchronous code finishes, JavaScript fully empties the microtask queue (promise callbacks) before ever touching the next macrotask — this is why promise handlers consistently run before the next setTimeout, even a 0ms one.
Call Stack Microtask Queue Macrotask Queue
| (promises) (setTimeout, events)
| | |
v v v
[ run sync code ] -> [ drain ALL microtasks ] -> [ run ONE macrotask ] -> repeatImportant Rules
Default to const; use let only when a variable must be reassigned; avoid var in new code entirely.
Prefer ===/!== over ==/!= to avoid surprising type coercion.
Use arrow functions for callbacks unless you specifically need a function's own this binding.
Reach for ?. and ?? when working with data that might be missing, instead of long chains of manual null checks.
Where this shows up
Every React component you'll write later in this course is a JavaScript (or TypeScript) function that relies on closures for state and effect hooks, destructuring for props, arrow functions for event handlers, and async/await for data fetching — the concepts in this lesson aren't background trivia, they're the exact mechanics React is built on.
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // logs 3, 3, 3 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // logs 0, 1, 2
var is function-scoped, so all three callbacks share the single final value of i. let creates a fresh block-scoped binding on each loop iteration, which each closure captures independently — this is one of the most common legacy var bugs.
[multiple-choice] Which variable declaration keyword is considered legacy and generally avoided in modern JavaScript? A. let B. const C. var D. static
C. var — var is function-scoped rather than block-scoped and has confusing hoisting behavior, so modern JavaScript prefers let and const.
[predict-output] In the event loop, which runs first after synchronous code finishes: a pending microtask (like a Promise .then) or a pending macrotask (like setTimeout)? A. The macrotask always runs first B. The microtask always runs first C. They run in the order they were scheduled, ignoring type D. It's random
B. The microtask always runs first — The JavaScript event loop always fully drains the microtask queue before moving on to the next macrotask, so promise callbacks consistently run before setTimeout callbacks.
[multiple-choice] What does the nullish coalescing operator (??) do differently from ||? A. It only falls back to the right-hand value when the left side is null or undefined, not for other falsy values like 0 or "" B. It always falls back regardless of the left-hand value C. It throws an error if the left side is falsy D. It is identical to || in every case
A. It only falls back to the right-hand value when the left side is null or undefined, not for other falsy values like 0 or "" — ?? checks specifically for null/undefined, whereas || falls back on any falsy value (0, "", NaN, false), which can cause bugs when 0 or "" are valid values.
Write a function fetchAndSummarize(userId) that fetches a user from /api/users/${userId}, destructures name and an optional role (defaulting to "member") from the response, and returns a string like "Ada (admin)". Use async/await, destructuring with a default value, and a template literal.
async function fetchAndSummarize(userId) { const res = await fetch(/api/users/${userId}); const { name, role = "member" } = await res.json(); return ${name} (${role}); }
Await the fetch, await .json() on the response, then destructure { name, role = "member" } out of the resulting object before building the return string.