Advanced JavaScript and TypeScriptIntroductionModern web applications are no longer collections of isolated scripts. They are distributed, event-driven systems that communicate with servers, manipulate browser resources, and coordinate many operations whose completion times are unpredictable. JavaScript provides the execution model used by much of the web platform, while TypeScript adds static analysis and expressive type modelling to improve reliability in large codebases. This chapter develops the concepts required to design maintainable browser applications: modules, scope and closures, prototypes, functional programming, asynchronous execution, promises, the event loop, browser APIs, error handling, and TypeScript. The emphasis is not merely on syntax, but on the runtime models and design decisions that explain why programs behave as they do. Learning ObjectivesAfter studying this chapter, a student should be able to: - organise applications with ES modules and understand module boundaries;
- explain lexical scope, closures, prototypes, and the prototype chain;
- apply functional patterns such as pure functions, immutability, composition, and higher-order functions;
- distinguish synchronous execution from asynchronous scheduling;
- use promises, async/await, cancellation, and structured error handling;
- explain the event loop and the ordering of tasks and microtasks;
- use browser APIs safely and efficiently;
- identify and handle operational, programmer, and user-input errors;
- create TypeScript types, interfaces, generics, unions, and type guards;
- evaluate trade-offs involving performance, security, maintainability, and accessibility.
1. JavaScript as a Language and RuntimeJavaScript has two related but distinct aspects. The language defines values, expressions, functions, objects, classes, modules, and control structures. The runtime environment supplies capabilities such as timers, network access, the DOM, storage, and event dispatch. A browser is therefore not the language itself; it is a host environment implementing JavaScript together with Web APIs. JavaScript is dynamically typed: a variable may refer to values of different types during execution. Its primitive values include strings, numbers, bigints, booleans, undefined, null, and symbols. Objects, arrays, functions, dates, maps, and sets are reference values. The distinction matters because assigning an object copies a reference, whereas assigning a primitive copies its value. const a = { count: 1 };
const b = a;
b.count = 2;
console.log(a.count); // 2The language is also lexical and predominantly single-threaded at the level of ordinary JavaScript execution. A function runs to completion before another JavaScript function executes on the same thread. Concurrency is achieved by the host environment, which schedules callbacks after external work has progressed. 2. Modules and Program StructureAn ES module is a file whose imports and exports form an explicit dependency boundary. Modules are evaluated in dependency order and are executed in strict mode. Their bindings are live: an imported binding reflects changes to the exported binding, although an importer cannot assign to that imported binding directly. // currency.js
export const taxRate = 0.15;
export function addTax(amount) {
return amount * (1 + taxRate);
}
// checkout.js
import { addTax } from './currency.js';
console.log(addTax(100));Named exports are useful when a module exposes several related operations. A default export represents the principal value of a module: export default class Cart {
constructor() { this.items = []; }
}A module should generally have high cohesion: its declarations should support a focused responsibility. It should also minimise accidental coupling. Importing a module solely for side effects can be valid—for example, registering a custom element—but such effects should be documented because they make program startup order significant. Dynamic import permits code splitting and conditional loading: button.addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart(data);
});This can reduce initial download cost, but it introduces a loading failure path and may delay the first interaction. Production systems should combine module design with bundling, caching, source maps, and content security policies. 3. Scope, Closures, and EncapsulationScope determines where an identifier can be accessed. let and const are block-scoped; var is function-scoped and is generally avoided in modern code. A function forms a lexical environment containing its parameters and local declarations. A closure occurs when a function retains access to variables from its defining environment after the outer function has returned. function makeCounter() {
let value = 0;
return {
increment() { value += 1; },
current() { return value; }
};
}
const counter = makeCounter();
counter.increment();
console.log(counter.current()); // 1The variable value is not directly accessible to client code, but the returned methods retain access to it. Closures therefore support encapsulation, factories, callbacks, memoization, and event handlers. A frequent issue is retaining unnecessary objects through long-lived callbacks. If a component registers a listener and is later removed without unregistering it, the listener may preserve references to the component's data. Modern applications should use explicit cleanup, AbortController, or framework lifecycle facilities. Closures capture bindings, not frozen snapshots. Consequently, callbacks created in a loop can observe a later value if the loop uses a shared var binding. let creates a new block-scoped binding for each iteration and normally avoids this problem. 4. Objects, Prototypes, and ClassesJavaScript uses prototype-based inheritance. Every ordinary object may have an internal prototype. If a property is not found on an object, the engine searches its prototype, then that prototype's prototype, until it reaches null. This sequence is the prototype chain. const animal = {
speak() { return 'sound'; }
};
const dog = Object.create(animal);
console.log(dog.speak()); // soundA method found through the prototype is shared rather than copied into every object. Constructor functions and the class syntax provide common ways to establish such relationships: class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}Class syntax does not replace prototypes; class methods are placed on the prototype. JavaScript classes support inheritance with extends, but composition is often preferable when behaviour does not represent a genuine “is-a” relationship. Excessive inheritance creates fragile coupling and makes substitution difficult. this is determined by the call form, not simply by where a function is written. In obj.method(), this is normally obj; in a standalone strict-mode call, it is undefined; an arrow function captures this lexically. call, apply, and bind can explicitly control the receiver.
5. Functional Programming PatternsFunctional programming treats computation as the transformation and combination of values. Important principles include: - Pure function: for the same inputs, it returns the same output and has no observable side effects.
- Immutability: existing values are not changed; new values are produced.
- Higher-order function: a function accepts or returns another function.
- Composition: small transformations are combined to form a larger operation.
const activeNames = users
.filter(user => user.active)
.map(user => user.name)
.sort(); map transforms each element, filter selects elements, and reduce combines elements into one result. A reducer should be carefully designed: mutating an accumulator can be efficient in controlled contexts, but immutable accumulation is easier to reason about and supports change detection in many UI systems.
A practical compromise is to keep domain calculations pure and isolate effects—network requests, logging, DOM updates, and storage—in clearly named boundary functions. This improves testing because pure logic can be tested without a browser or server. Memoization caches a function's output for previously seen inputs. It is appropriate only when the function is sufficiently expensive, inputs are stable, and cache growth is controlled. Caching a result that depends on hidden mutable state produces stale or incorrect behaviour. 6. Asynchronous Programming and PromisesAsynchronous programming allows an application to begin an operation and continue other work while waiting for a timer, network response, or user action. A promise represents the eventual result of an operation. It is pending initially and becomes fulfilled or rejected once; settlement is permanent. function loadUser(id) {
return fetch(`/api/users/${encodeURIComponent(id)}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
});
}
loadUser('42')
.then(user => renderUser(user))
.catch(error => showError(error));fetch rejects for network-level failures, such as an unavailable connection, but usually does not reject merely because the server returns HTTP 404 or 500. The application must inspect response.ok or response.status.
async functions always return promises. await pauses that function until a promise settles; it does not block the browser's entire JavaScript thread.
async function displayUser(id) {
try {
const user = await loadUser(id);
renderUser(user);
} catch (error) {
report(error);
}
}Independent operations should not be awaited unnecessarily in sequence: const [profile, messages] = await Promise.all([
loadProfile(),
loadMessages()
]); Promise.all rejects when any input rejects. Promise.allSettled waits for every input and reports individual outcomes. Promise.race settles with the first settled promise, while Promise.any fulfills with the first fulfillment and rejects only when all inputs reject.
Cancellation is not automatic. An AbortController can signal cancellation to APIs that support it: const controller = new AbortController();
const request = fetch('/api/report', { signal: controller.signal });
controller.abort();Applications should distinguish cancellation from a genuine failure so that an expected user navigation does not produce an alarming error message. 7. The Event LoopThe event loop explains how asynchronous callbacks enter JavaScript execution. A simplified browser model includes a call stack, host-managed tasks, and a microtask queue. A task may be a timer callback, user event, or completion callback supplied by a host API. Promise reactions and queueMicrotask callbacks are microtasks. After the current stack becomes empty, the runtime drains available microtasks before proceeding to another task and, at suitable points, rendering the page. Therefore microtasks have higher scheduling priority than later timer tasks. console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');The output is A, D, C, B. The timer does not mean “execute immediately after zero milliseconds”; it means “make the callback eligible after at least the relevant delay and scheduling conditions.” A long synchronous loop blocks input, rendering, and other callbacks. Repeatedly scheduling microtasks can also starve tasks because the microtask queue is drained before the next task. Expensive work should be divided into chunks, moved to a Web Worker where appropriate, or scheduled using mechanisms such as requestAnimationFrame for visual updates and requestIdleCallback where supported for lower-priority work. 8. Browser APIs and Resource ManagementThe DOM represents a document as a tree of nodes. Efficient code minimises unnecessary layout-triggering operations and batches DOM updates. textContent is safer than innerHTML when inserting untrusted text because it does not interpret the text as markup. Event delegation attaches one listener to a stable ancestor and uses event bubbling to handle descendants: list.addEventListener('click', event => {
const item = event.target.closest('[data-id]');
if (!item) return;
removeItem(item.dataset.id);
});The History API supports client-side navigation, while localStorage provides persistent string-based storage. Neither should store highly sensitive secrets in an ordinary web application because scripts exposed to cross-site scripting can often access them. IndexedDB is more suitable for larger structured client-side data. Other important APIs include URL and URLSearchParams for safe URL construction, IntersectionObserver for visibility-based work, Web Workers for parallel background computation, and the Notifications or Geolocation APIs, which require user permission and responsible disclosure. Browser security boundaries are central to API use. Same-origin policy restricts many cross-origin interactions. CORS is a server-controlled mechanism that permits selected cross-origin requests; it is not a client-side method for bypassing security. Never treat client-side validation as a substitute for server-side authorization or validation. 9. Error Handling and ReliabilityErrors may be classified as: - Programmer errors, such as accessing an undefined value due to a coding defect.
- Operational errors, such as a timeout or unavailable server.
- Input errors, in which a user or external system supplies invalid data.
try...catch...finally handles synchronous exceptions and exceptions observed through await. Promise chains require a rejection handler. Errors should be enriched with useful context but should not expose credentials, tokens, or personal data.
async function saveOrder(order) {
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order)
});
if (!response.ok) throw new Error('Order service rejected request');
return await response.json();
} catch (error) {
console.error('saveOrder failed', { cause: error });
throw new Error('The order could not be saved', { cause: error });
}
}An application should provide a user-appropriate message, log diagnostic details to a controlled observability system, and define recovery behaviour. Retrying is safe only when the operation is idempotent or protected by an idempotency key. Exponential backoff reduces pressure on a failing service, but retries require limits and should respect cancellation. 10. TypeScript: Static Modelling for JavaScriptTypeScript is a language that adds static type checking and compiles to JavaScript. Types are erased from the emitted runtime program; therefore TypeScript annotations do not validate data received from a network at runtime. External data still requires validation. interface Product {
readonly id: string;
name: string;
price: number;
}
function priceWithTax(product: Product, rate: number): number {
return product.price * (1 + rate);
}An interface describes a structural contract. Type aliases are useful for unions and compositions: type Status = 'idle' | 'loading' | 'success' | 'error';
type Result<T> =
| { status: 'success'; value: T }
| { status: 'error'; message: string };A discriminated union makes invalid states harder to represent. Type narrowing uses control flow, typeof, property checks, or custom type predicates: function isString(value: unknown): value is string {
return typeof value === 'string';
}unknown is safer than any for untrusted values because it requires narrowing before use. any disables much of the compiler's protection and should be confined to carefully documented boundaries.
Generics express relationships between types: function first<T>(items: T[]): T | undefined {
return items[0];
}The type parameter T preserves the element type without duplicating the function for strings, numbers, or objects. Generic constraints restrict acceptable types: function getId<T extends { id: string }>(item: T): string {
return item.id;
}Compiler options such as strict, noImplicitAny, and strictNullChecks identify defects early. TypeScript cannot prove that every runtime value is safe, and inaccurate type assertions merely hide uncertainty. Prefer validation at system boundaries, precise domain types, and small modules. TypeScript and Asynchronous CodePromises can be parameterised: async function getProduct(id: string): Promise<Product> {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) throw new Error('Request failed');
const data: unknown = await response.json();
return parseProduct(data);
}The function should not simply assert that parsed JSON is a Product; it should validate the structure. A schema-validation library may be used in professional systems, or a handwritten guard may be sufficient for a small boundary. 11. Illustrative Case: Reliable Product SearchBackgroundAn online retailer has a search page that requests products as the user types. The initial implementation sends a request for every keystroke, allows older responses to overwrite newer results, and displays a generic blank page when the server fails. Problem AnalysisThe design has three race conditions or usability risks. First, requests may complete out of order. Second, unnecessary requests consume bandwidth and server capacity. Third, a slow response blocks neither the browser nor the user, but the interface provides no loading, empty, or error state. Approach and SolutionA typed controller models the states explicitly. Input is debounced so that a request begins only after a short period of inactivity. Each new request aborts the previous request. The response is validated before rendering, and rendering uses text insertion for untrusted product names. type SearchState<T> =
| { kind: 'idle' }
| { kind: 'loading'; query: string }
| { kind: 'success'; items: T[] }
| { kind: 'empty' }
| { kind: 'failure'; message: string };
interface SearchProduct {
id: string;
name: string;
price: number;
}
let controller: AbortController | undefined;
async function search(query: string): Promise<SearchProduct[]> {
controller?.abort();
controller = new AbortController();
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
});
if (!response.ok) throw new Error('Search service unavailable');
const data: unknown = await response.json();
if (!Array.isArray(data)) throw new Error('Invalid search response');
return data.filter(isSearchProduct);
}
function isSearchProduct(value: unknown): value is SearchProduct {
if (typeof value !== 'object' || value === null) return false;
const item = value as Record<string, unknown>;
return typeof item.id === 'string' &&
typeof item.name === 'string' &&
typeof item.price === 'number';
}The user interface can represent each SearchState and ignore an AbortError as an expected cancellation. A production implementation would add debouncing, request limits, accessibility announcements for result counts, keyboard support, server-side rate limiting, and monitoring. Outcome and Learning PointsThe revised design prevents stale results, reduces unnecessary work, makes invalid data visible, and gives users meaningful feedback. The case demonstrates that asynchronous correctness is a combination of scheduling, cancellation, state modelling, validation, and communication—not merely the use of await. 12. Professional Practice, Ethics, and Contemporary DevelopmentAdvanced web development has consequences beyond functional correctness. Applications should minimise collection of personal data, request only necessary permissions, protect sessions, use secure transport, and respect accessibility standards. Error telemetry must avoid leaking sensitive values. Third-party scripts increase supply-chain risk and should be inventoried, pinned or integrity-protected where appropriate, and reviewed. AI-assisted coding can accelerate prototyping, but generated code may contain insecure assumptions, incorrect types, or licensing concerns. Developers remain responsible for testing, review, dependency auditing, accessibility, and data protection. Static typing improves maintainability but does not replace threat modelling, tests, runtime validation, or code review. Current practice increasingly combines TypeScript with component frameworks, server-side rendering, edge runtimes, Web Workers, streams, and typed API contracts. These developments do not invalidate the underlying principles: explicit boundaries, controlled effects, predictable state transitions, cancellation, validation, and observability remain fundamental. Chapter SummaryJavaScript applications depend on both a language model and a host runtime. ES modules establish explicit dependency boundaries, while closures provide lexical encapsulation and support factories and callbacks. Prototypes explain property lookup and class behaviour; composition often produces more flexible designs than deep inheritance. Functional patterns isolate pure transformations from side effects. Asynchronous operations are represented by promises and coordinated through async/await, cancellation, and combinators such as Promise.all. The event loop schedules tasks and microtasks, explaining callback ordering and the dangers of blocking work. Browser APIs require deliberate resource management and security awareness. Robust error handling distinguishes failure types, provides recovery, and avoids disclosure of sensitive information. TypeScript supplies compile-time models—interfaces, unions, generics, narrowing, and strict compiler settings—but runtime data must still be validated. The product-search case shows how typed state, cancellation, validation, and user feedback combine into a reliable web feature. Key TermsClosure: A function together with access to its lexical environment. ES module: A JavaScript file with explicit import and export declarations. Prototype chain: The sequence of objects searched during property lookup. Lexical scope: Scope determined by the written structure of a program. Pure function: A function without observable side effects that gives consistent results for the same inputs. Higher-order function: A function that accepts or returns another function. Promise: An object representing the eventual success or failure of an asynchronous operation. Async/await: Syntax for writing promise-based asynchronous control flow. Task: A host-scheduled unit of work such as a timer or user-event callback. Microtask: High-priority queued work such as a promise reaction. Event loop: The runtime mechanism that coordinates stacks, tasks, microtasks, and rendering. AbortController: An API for signalling cancellation to compatible operations. Event delegation: Handling descendant events through a listener on an ancestor. TypeScript: A statically checked language that compiles to JavaScript. Interface: A TypeScript description of an object-shaped contract. Union type: A type allowing a value to belong to one of several alternatives. Type guard: Logic that narrows an uncertain value to a more specific type. Generic: A parameterised type or function that preserves relationships among types. Idempotent operation: An operation that can be repeated without changing the intended result beyond the first application. Review QuestionsShort Answer Questions- What is a closure, and how can it support encapsulation?
- Why does
fetch usually require explicit checking of response.ok? - Distinguish a task from a microtask in the browser event loop.
- What is the purpose of a discriminated union in TypeScript?
- Why should untrusted JSON not be accepted solely through a TypeScript type assertion?
Long Answer Questions- Explain ES modules, named exports, default exports, dynamic imports, and the effect of module boundaries on maintainability.
- Describe prototype-based inheritance and compare composition with class inheritance.
- Explain promise states,
async/await, promise combinators, and cancellation with examples. - Discuss browser event processing, including the call stack, tasks, microtasks, rendering, and starvation.
- Explain how TypeScript's interfaces, generics, unions, narrowing, and strict compiler settings improve application quality while leaving runtime validation necessary.
Analytical / Application Questions- A search interface displays an older response after a newer query has completed. Diagnose the cause and design a solution using cancellation or request identity.
- A page becomes unresponsive while processing 100,000 records in one callback. Analyse the event-loop consequences and propose at least two technically justified solutions.
- Design a typed state model for a file-upload component that includes idle, uploading, success, cancellation, and failure states. Explain how the model prevents invalid UI assumptions.
Multiple Choice Questions with AnswersQuestion 1Which statement best describes a JavaScript closure? A. A function that can access variables from its defining lexical environment B. A class that inherits directly from another class C. A promise that has already been rejected D. A browser event that bubbles to the document Correct Answer: A. A function that can access variables from its defining lexical environment Question 2Which output order is produced by the following code: console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');? A. A, B, C, D B. A, D, C, B C. D, A, B, C D. A, C, D, B Correct Answer: B. A, D, C, B Question 3What does Promise.all do when one of its input promises rejects? A. It waits for every promise and always fulfills B. It fulfills with only the successful values C. It rejects as soon as the combined operation is rejected D. It retries the rejected promise automatically Correct Answer: C. It rejects as soon as the combined operation is rejected Question 4Why should a program check response.ok after using fetch? A. fetch rejects automatically for every HTTP error B. response.ok indicates whether the HTTP response is in the successful range C. response.ok converts JSON into a TypeScript object D. Checking response.ok cancels the request Correct Answer: B. response.ok indicates whether the HTTP response is in the successful range Question 5Which TypeScript type is safest for an external value that has not yet been validated? A. any B. never C. unknown D. void Correct Answer: C. unknown Question 6What is the principal benefit of a discriminated union? A. It disables type checking for related objects B. It enables control-flow narrowing based on a shared distinguishing property C. It guarantees runtime validation of JSON D. It makes all object properties mutable Correct Answer: B. It enables control-flow narrowing based on a shared distinguishing property Question 7Which technique is most appropriate for preventing a previous search request from overwriting newer results? A. Use cancellation or associate responses with the request that created them B. Convert all responses to strings C. Remove all error handling D. Use a global variable for every response Correct Answer: A. Use cancellation or associate responses with the request that created them Question 8What does an arrow function normally do with this? A. It creates a new dynamic receiver on every call B. It captures this lexically from its surrounding context C. It always binds this to window D. It disables access to object properties Correct Answer: B. It captures this lexically from its surrounding context Question 9Which statement about TypeScript annotations is correct? A. They remain as runtime validation in ordinary emitted JavaScript B. They automatically sanitize network responses C. They are generally erased during compilation, so external data still needs validation D. They prevent all programmer errors Correct Answer: C. They are generally erased during compilation, so external data still needs validation Question 10Which approach is most suitable for expensive synchronous work that would block the browser interface? A. Run it in one larger callback B. Move it to a Web Worker or divide it into scheduled chunks C. Replace all variables with var D. Put it inside a microtask loop Correct Answer: B. Move it to a Web Worker or divide it into scheduled chunks
|