Learn/software dev/JavaScript & TypeScript
Intermediate~20 min read

JavaScript & TypeScript

Modern JavaScript and TypeScript: scoping and hoisting, closures, arrow functions and this, promises and async/await, destructuring and spread, ES modules, array methods, and the TypeScript type system with the event loop.

ES2026TypesAsyncClosures

Variables & Scoping

JavaScript has three declaration keywords, but modern code uses only two. Prefer const by default and let when reassignment is needed. var is legacy and should be avoided.

KeywordScopeReassignHoisting
constBlockNoTDZ until declared
letBlockYesTDZ until declared
varFunctionYesHoisted, init undefined

Hoisting moves declarations to the top of their scope during compilation. var declarations are hoisted and initialised to undefined; let and const are hoisted too but sit in the Temporal Dead Zone (TDZ) — accessing them before declaration throws a ReferenceError.

javascript
console.log(a); // undefined  — var is hoisted & initialised
var a = 1;

console.log(b); // ReferenceError — b is in the TDZ
let b = 2;

// const requires initialisation and cannot be reassigned
const PI = 3.14159;
// PI = 3;  // TypeError: Assignment to constant variable
// Note: const objects are still mutable
const cfg = { debug: true };
cfg.debug = false; // fine — the binding is constant, not the value

Closures

A closure is a function bundled with references to its surrounding lexical scope. The inner function keeps access to outer variables even after the outer function returns. Closures power data privacy, memoisation, and factory functions.

javascript
function makeCounter() {
  let count = 0;                 // private state
  return {
    inc: () => ++count,
    get: () => count,
  };
}
const c = makeCounter();
c.inc(); c.inc();
c.get(); // 2  — count is not accessible from outside

// Classic loop bug: var shares one binding, let creates one per iteration
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0 1 2

Why it matters

Every React hook relies on closures — useState, useEffect, and event handlers close over props and state from the render they were created in. "Stale closure" bugs come from a handler capturing an old value.

Functions, Arrow Functions & this

Arrow functions do not bind their own this, arguments, or super. They inherit this lexically from where they are defined. Regular functions bind this based on how they are called.

javascript
const timer = {
  seconds: 0,
  startBroken() {
    // regular function: `this` is undefined/window in the callback
    setInterval(function () { this.seconds++; }, 1000);
  },
  startFixed() {
    // arrow inherits `this` from startFixed -> the timer object
    setInterval(() => { this.seconds++; }, 1000);
  },
};

// Explicit binding
function greet(greeting) { return `${greeting}, ${this.name}`; }
greet.call({ name: 'Ada' }, 'Hi');       // "Hi, Ada"
greet.apply({ name: 'Ada' }, ['Hello']); // "Hello, Ada"
const bound = greet.bind({ name: 'Ada' });
bound('Hey');                            // "Hey, Ada"

Destructuring & Spread

Destructuring pulls values out of arrays and objects into variables. The spread operator ... expands iterables; the same syntax in a parameter list or destructuring pattern is the rest operator, collecting the remainder.

javascript
// Object destructuring with defaults, renaming, and nesting
const user = { id: 1, name: 'Ada', address: { city: 'London' } };
const { name, role = 'member', address: { city } } = user;

// Array destructuring + swap
let [first, , third] = [10, 20, 30]; // first=10, third=30
let x = 1, y = 2;
[x, y] = [y, x];                     // swap without temp

// Rest gathers the remainder
const [head, ...tail] = [1, 2, 3, 4]; // head=1, tail=[2,3,4]
const { id, ...rest } = user;         // rest = everything but id

// Spread clones & merges (shallow)
const merged = { ...user, role: 'admin' };
const combined = [...[1, 2], ...[3, 4]]; // [1,2,3,4]

Array Methods

Prefer declarative iteration methods over manual loops. map transforms, filter selects, and reduce folds a collection into a single value. All return new arrays/values and do not mutate the source.

javascript
const orders = [
  { id: 1, total: 30, paid: true },
  { id: 2, total: 50, paid: false },
  { id: 3, total: 20, paid: true },
];

const ids     = orders.map(o => o.id);          // [1, 2, 3]
const paid    = orders.filter(o => o.paid);     // two orders
const revenue = orders
  .filter(o => o.paid)
  .reduce((sum, o) => sum + o.total, 0);         // 50

// Other essentials
orders.find(o => o.total > 40);   // first match or undefined
orders.some(o => !o.paid);        // true if any match
orders.every(o => o.total > 0);   // true if all match
orders.flatMap(o => [o.id, o.total]); // map then flatten one level
[3, 1, 2].toSorted((a, b) => a - b); // ES2023: non-mutating sort

Promises & async/await

A Promise represents a future value in one of three states: pending, fulfilled, or rejected. async/await is syntactic sugar over promises that lets you write asynchronous code in a synchronous style. Always wrap awaited calls in try/catch.

javascript
async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('Failed to load user', err);
    throw err; // re-throw so callers can react
  }
}

// Run independent work concurrently — do NOT await in a loop serially
const [user, posts] = await Promise.all([
  loadUser(1),
  fetch('/api/posts').then(r => r.json()),
]);

// Combinators
Promise.allSettled(tasks); // waits for all, never short-circuits
Promise.race(tasks);       // first to settle (fulfil OR reject)
Promise.any(tasks);        // first to fulfil, ignores rejections

Common trap

await inside a forEach does not wait — forEach ignores returned promises. Use a for...of loop for sequential work, or Promise.all(arr.map(...)) for concurrent work.

The Event Loop

JavaScript is single-threaded. The event loop runs synchronous code on the call stack, then drains the microtask queue (promise callbacks, queueMicrotask) completely, then processes one macrotask (timers, I/O, events) — repeating this cycle. Microtasks always run before the next macrotask.

javascript
console.log('1: sync');
setTimeout(() => console.log('4: timeout (macrotask)'), 0);
Promise.resolve().then(() => console.log('3: promise (microtask)'));
console.log('2: sync');
// Output order: 1, 2, 3, 4

ES Modules

ES Modules (ESM) are the standard. Each file is its own scope; you explicitly export and import bindings. Imports are hoisted and live (they reflect later changes to the exported binding).

javascript
// math.js — named + default exports
export const add = (a, b) => a + b;
export function mul(a, b) { return a * b; }
export default function sub(a, b) { return a - b; }

// app.js — importing
import sub, { add, mul } from './math.js';
import * as math from './math.js';   // namespace import
import { add as sum } from './math.js'; // rename

// Dynamic import returns a promise — great for code splitting
const { default: heavy } = await import('./heavy.js');

TypeScript: Types, Interfaces & Unions

TypeScript adds a static type layer that is erased at compile time. Use interface for object shapes you may extend or that model an API; use type for unions, tuples, and computed types.

Featureinterfacetype
Object shapesYesYes
Unions / tuplesNoYes
Declaration mergingYesNo
extends / & intersectextends& intersection
typescript
interface User {
  id: number;
  name: string;
  role?: 'admin' | 'member';   // optional + literal union
}

type Result<T> =
  | { ok: true; data: T }
  | { ok: false; error: string }; // discriminated union

// Literal & union types
type Status = 'idle' | 'loading' | 'success' | 'error';

Generics

Generics let you write reusable, type-safe code parameterised over types — Array<T> is itself a generic. Use constraints with extends to require capabilities.

typescript
function first<T>(arr: Array<T>): T | undefined {
  return arr[0];
}
const n = first<number>([1, 2, 3]); // number | undefined

// Constrained generic — T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

Utility Types

Built-in utility types transform existing types instead of duplicating them.

typescript
interface Todo { id: number; title: string; done: boolean; }

type TodoDraft   = Partial<Todo>;             // all props optional
type TodoKeys    = Pick<Todo, 'id' | 'title'>; // subset of props
type TodoNoId    = Omit<Todo, 'id'>;          // remove props
type ReadTodo    = Readonly<Todo>;           // all props readonly
type StatusMap   = Record<string, boolean>;  // key/value map
type Required2   = Required<TodoDraft>;       // all props required

Narrowing, Enums & Strict Mode

Narrowing uses control-flow analysis to refine a broad type to a specific one via typeof, in, instanceof, or discriminant checks. Prefer union literals over enum in modern code; use const enum or as const objects when you need runtime values.

typescript
function area(shape: Result<number>) {
  if (shape.ok) return shape.data;   // narrowed to the ok branch
  return shape.error;                // narrowed to the error branch
}

function format(x: string | number) {
  if (typeof x === 'string') return x.trim(); // x is string here
  return x.toFixed(2);                          // x is number here
}

// Modern alternative to enums
const Direction = { Up: 'UP', Down: 'DOWN' } as const;
type Direction = typeof Direction[keyof typeof Direction]; // 'UP' | 'DOWN'

Always enable strict mode

Set "strict": true in tsconfig.json. It turns on strictNullChecks, noImplicitAny, and more — catching null bugs and untyped values at compile time. Add noUncheckedIndexedAccess for even safer array/object access.

Practice Exercises

  1. Predict the output of a loop using var vs let with setTimeout, then explain why using closures.
  2. Write a memoize(fn) higher-order function that caches results using a closure and a Map.
  3. Implement groupBy(arr, keyFn) using reduce, and type it generically in TypeScript.
  4. Order the log output of a snippet mixing synchronous code, setTimeout(0), and Promise.resolve().then.
  5. Define a discriminated union Result<T> and a function that exhaustively narrows it, using a never check in the default branch.
  6. Given an interface User, derive a "create payload" type that omits the server-generated id and makes timestamps optional using Omit and Partial.

Section navigation