Frontend Development

Chapter 4
JavaScript and TypeScript Foundations

Certificate in Full stack programming
Frontend Development   •   Certificate

JavaScript and TypeScript Foundations

Introduction

Modern frontend applications are interactive programs. They respond to user actions, retrieve and transform data, validate input, update the Document Object Model (DOM), and communicate with services. HTML provides structure and CSS provides presentation, but JavaScript supplies most of the behaviour. TypeScript extends JavaScript with a static type system that helps developers detect many mistakes before code runs.

This chapter develops the foundations required to implement reliable interface logic. It treats JavaScript as a programming language and TypeScript as a development tool rather than as a collection of isolated syntax rules. Examples use modern ECMAScript conventions and browser-oriented programming practices.

Learning Objectives

By the end of this chapter, you should be able to:

  • declare and update variables safely;
  • distinguish primitive and reference values;
  • use operators, conditions, loops, and functions;
  • manipulate arrays, objects, maps, and sets;
  • organise code with modules;
  • handle failures using exceptions and result-oriented techniques;
  • explain JavaScript's asynchronous model at an introductory level;
  • apply essential TypeScript types, interfaces, unions, and generics;
  • design small interface features using readable and maintainable logic.

1. JavaScript as a Language

JavaScript is dynamically typed: a variable may hold values of different types during execution. It is also interpreted or just-in-time compiled by a JavaScript engine such as V8, SpiderMonkey, or JavaScriptCore. The language follows the ECMAScript standard, while browsers provide additional APIs such as document, fetch, and localStorage.

A useful distinction is:

  • Language features: variables, functions, objects, promises, and modules.
  • Host APIs: browser capabilities such as the DOM, timers, and network requests.
  • Tools: TypeScript, linters, formatters, bundlers, and test frameworks.

JavaScript executes statements in order, but functions, callbacks, promises, and events allow work to be scheduled for later. This explains why browser code must be designed around both immediate computation and asynchronous events.

2. Variables, Values, and Data Types

Use const by default and let when reassignment is necessary. Avoid var in new code because its function-scoped behaviour and hoisting rules can make programs harder to reason about.

const taxRate = 0.15;
let quantity = 2;
quantity = quantity + 1;

const prevents reassignment of the variable binding; it does not make an object immutable:

const user = { name: "Amina" };
user.name = "Ravi";       // allowed
// user = { name: "Liu" }; // error

Primitive types

JavaScript's principal primitive types are:

TypeExamplePurpose
string"Save"Text
number42, 3.5Integer and floating-point values
bigint9007199254740993nVery large integers
booleantrueLogical state
undefinedundefinedMissing assigned value
symbolSymbol("id")Unique identifiers
nullnullIntentional absence of an object value

Objects, arrays, functions, dates, and regular expressions are non-primitive values. Variables hold references to these values, so two variables can refer to the same object.

const first = { completed: false };
const second = first;
second.completed = true;
console.log(first.completed); // true

Use typeof for basic inspection, but note that typeof null returns "object", a historical language quirk. Array.isArray(value) is the reliable way to identify arrays.

Equality and conversion

Prefer strict equality (=== and !==) because it compares both value and type. Loose equality (==) performs implicit conversion and can produce surprising results.

0 === false; // false
0 == false;  // true

Convert deliberately with Number(value), String(value), or Boolean(value). Values such as 0, "", null, undefined, and NaN are falsy; most other values are truthy. Do not confuse a falsy value with an absent value when 0 or an empty string is valid input.

3. Operators and Expressions

Operators construct expressions that produce values.

  • Arithmetic: +, -, , /, %, and *.
  • Assignment: =, +=, -=, and related forms.
  • Comparison: >, <, >=, <=, ===, and !==.
  • Logical: &&, ||, and !.
  • Nullish coalescing: ??, which uses a fallback only for null or undefined.
  • Conditional: condition ? valueIfTrue : valueIfFalse.
  • Optional chaining: object?.property, which safely stops when a value is nullish.
const displayName = account?.profile?.name ?? "Guest";

Logical operators return operands, not necessarily booleans. Therefore a || b returns a if it is truthy, otherwise b; a ?? b preserves valid falsy values such as 0 and false.

Floating-point numbers follow the IEEE 754 double-precision model. Consequently, some decimal calculations are approximate:

0.1 + 0.2 === 0.3; // false

For currency, store minor units such as cents as integers where possible, or use a suitable decimal library for high-precision financial work.

4. Control Flow

Conditions

Use if...else for branching and switch when comparing one expression with several discrete cases.

function labelForStatus(status) {
  if (status === "paid") return "Complete";
  if (status === "pending") return "Awaiting payment";
  return "Unknown status";
}

A switch should normally include break, return, or another deliberate control transfer to avoid accidental fall-through.

Loops

Use for...of to iterate over values, for...in mainly for object keys, and array methods when they communicate intent clearly.

for (const item of cart) {
  console.log(item.price);
}

break exits a loop and continue skips to the next iteration. Avoid deeply nested loops and unclear mutation; extract a function when a loop becomes difficult to understand.

5. Functions and Scope

Functions package behaviour and support reuse, testing, and abstraction. Parameters receive values, and return supplies a result. A function that does not explicitly return a value returns undefined.

function subtotal(price, quantity) {
  return price * quantity;
}

const total = (price, quantity) => price * quantity;

Arrow functions have concise syntax and capture the surrounding this; ordinary methods have their own call-dependent this. In interface code, prefer explicit parameters and avoid relying on unfamiliar this behaviour.

JavaScript uses lexical scope. A nested function can access variables in its surrounding scope, forming a closure. Closures are useful for private state and event handlers, but retaining large objects unnecessarily can cause memory pressure.

Default and rest parameters improve function interfaces:

function greet(name = "Visitor") {
  return `Hello, ${name}`;
}

function addAll(...values) {
  return values.reduce((sum, value) => sum + value, 0);
}

Good functions generally have one clear responsibility, predictable inputs and outputs, and limited side effects. A function that changes the DOM, performs network access, and calculates totals simultaneously is harder to test than separate functions for each concern.

6. Collections and Data Transformation

Arrays

Arrays are ordered, zero-indexed collections. Common methods include:

  • map: transforms every element and returns a new array;
  • filter: retains elements satisfying a predicate;
  • find: returns the first matching element;
  • some and every: test collection predicates;
  • reduce: combines elements into one result;
  • sort: reorders the array in place.
const prices = [12, 8, 15];
const expensive = prices.filter(price => price > 10);
const total = prices.reduce((sum, price) => sum + price, 0);

The default sort() compares string representations. For numbers, provide a comparator:

prices.sort((a, b) => a - b);

Methods such as map and filter usually support immutable-style programming by returning new arrays. This reduces unintended side effects and works well with UI state management.

Objects

Objects represent related properties and behaviour. Use dot notation for known property names and bracket notation for dynamic names.

const key = "email";
const profile = { name: "Mira", email: "[email protected]" };
console.log(profile[key]);

Destructuring extracts values:

const { name, email } = profile;
const [first, second] = ["HTML", "CSS"];

Spread syntax creates shallow copies:

const updated = { ...profile, verified: true };

A shallow copy does not clone nested objects. For structured cloning, use structuredClone where supported or a carefully chosen serialization/library strategy.

Maps and sets

Map stores key–value pairs and supports keys of any type. Set stores unique values. They are preferable to plain objects when key semantics or uniqueness are central.

const selectedIds = new Set([2, 4, 2]); // {2, 4}
const scores = new Map([["Amina", 86]]);

7. Modules and Program Organisation

ES modules divide a program into files with explicit imports and exports.

// currency.js
export function formatCurrency(amount) {
  return new Intl.NumberFormat("en-US", {
    style: "currency", currency: "USD"
  }).format(amount);
}

// checkout.js
import { formatCurrency } from "./currency.js";

Named exports are clear when a module provides several capabilities. Default exports can represent one principal value but may make renaming easier and consistency weaker. Keep modules cohesive: a validation module should not also manage unrelated visual effects.

Modules create boundaries that improve reuse, testing, dependency analysis, and team collaboration. In browsers, scripts generally require type="module"; production tools often bundle modules and optimise delivery.

8. Errors and Reliable Failure Handling

Errors may arise from invalid user input, failed network requests, unavailable resources, or programming defects. Use throw to signal failure and try...catch...finally to handle it at an appropriate boundary.

async function loadProfile(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`Request failed: ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error(error);
    throw new Error("Unable to load profile", { cause: error });
  }
}

fetch rejects for network-level failures but does not automatically reject for HTTP statuses such as 404 or 500; checking response.ok is therefore important. Catch errors where the program can add useful context or present a meaningful recovery option. Do not silently suppress exceptions.

Validate external data at the boundary. Data from forms, URLs, local storage, and servers should be treated as untrusted, even when a TypeScript type appears to describe it. Types disappear at runtime and cannot validate JSON automatically.

9. Asynchronous JavaScript

JavaScript uses an event loop to coordinate synchronous execution, task queues, and asynchronous callbacks. A promise represents a future result with states pending, fulfilled, or rejected. async functions return promises, and await makes promise-based code easier to read.

async function saveForm(data) {
  const response = await fetch("/api/forms", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data)
  });
  if (!response.ok) throw new Error("Save failed");
  return response.json();
}

Independent requests can run concurrently with Promise.all; it rejects if any included promise rejects. Use Promise.allSettled when every result, including failures, must be inspected. Disable repeated submission controls where appropriate, and design loading, success, and failure states explicitly.

10. Essential TypeScript

TypeScript is a superset of JavaScript that checks types before execution and then emits JavaScript. It improves editor assistance, refactoring, and documentation, but it does not replace runtime validation.

let username: string = "Noah";
let attempts: number = 0;
let enabled: boolean = true;

Type inference often makes annotations unnecessary:

const pageSize = 20; // inferred as number

Use interfaces or type aliases to describe structures:

interface Product {
  id: number;
  name: string;
  price: number;
  description?: string;
}

type ProductId = number | string;

The ? marks an optional property. A union means a value can be one of several types. Narrow a union before using a type-specific operation:

function formatId(id: ProductId): string {
  return typeof id === "number" ? id.toString() : id.toUpperCase();
}

unknown is safer than any for values whose type is not known because it requires narrowing. any disables checking and should be limited to carefully isolated legacy or interoperation code. The never type can represent code paths that cannot complete normally, such as a function that always throws.

Generics express relationships between types:

function first<T>(items: T[]): T | undefined {
  return items[0];
}

Here, the returned type corresponds to the element type supplied by the caller. Type assertions such as value as Product tell the compiler to trust the developer; they do not perform runtime checking and should not be used as a substitute for validation.

TypeScript and the DOM

DOM APIs may return null, so strict TypeScript configuration encourages safe handling:

const button = document.querySelector<HTMLButtonElement>("#save");
button?.addEventListener("click", () => {
  console.log("saved");
});

A practical tsconfig commonly enables strict checking, including strictNullChecks, because detecting missing values early prevents many runtime failures.

11. Applied Case: A Typed Shopping-Cart Interaction

The following is an illustrative case. A small online shop needs a cart that displays items, calculates a total, prevents invalid quantities, and reports failures when saving.

Background and problem

The first implementation stores quantities as strings from form controls, mutates shared objects, and assumes that the server always succeeds. Users see incorrect totals and receive no useful feedback after a failed save.

Analysis

The central risks are weak input validation, unclear ownership of state, floating-point currency handling, and inadequate asynchronous error handling. A maintainable design separates domain logic from rendering and network access.

Approach

interface CartItem {
  id: number;
  name: string;
  unitPriceCents: number;
  quantity: number;
}

function cartTotalCents(items: CartItem[]): number {
  return items.reduce(
    (total, item) => total + item.unitPriceCents * item.quantity,
    0
  );
}

function updateQuantity(item: CartItem, rawValue: string): CartItem {
  const parsed = Number(rawValue);
  if (!Number.isInteger(parsed) || parsed < 1) {
    throw new Error("Quantity must be a positive whole number");
  }
  return { ...item, quantity: parsed };
}

The price is stored in cents, avoiding common decimal representation problems for ordinary currency calculations. The update function returns a new object rather than mutating the existing item. The interface layer can catch validation errors, display a message, and render the revised state. A separate service function can save the cart and check the HTTP response.

Outcome and learning points

This design provides explicit data contracts, predictable calculations, isolated validation, and a clear place for error recovery. It also makes unit testing straightforward: given known items, cartTotalCents can be tested without a browser or server. In a production system, the server must repeat validation because client-side checks can be bypassed.

12. Professional Practices, Security, and Contemporary Development

Readable names, small functions, consistent formatting, linting, automated tests, and code review are engineering controls, not cosmetic preferences. Use semantic HTML and accessible event interactions rather than assuming every user operates a pointer. Never insert untrusted strings with innerHTML; prefer textContent or a trusted sanitisation strategy to reduce cross-site scripting risk.

Do not place secrets such as private API keys in frontend bundles. Apply authentication, authorisation, validation, and rate limiting on the server. Protect sensitive data in storage and avoid logging personal information unnecessarily. Dependency updates should be monitored because third-party packages can introduce vulnerabilities.

AI coding assistants can accelerate scaffolding and explanation, but generated code must be reviewed for correctness, licensing concerns, insecure patterns, accessibility, and inappropriate data disclosure. Type checking, tests, and human review remain necessary.

Modern frontend development increasingly uses component frameworks, build tools, strict TypeScript, schema-validation libraries, and automated testing. These tools are valuable only when foundational language concepts remain understood. Abstraction should reduce complexity, not conceal it.

Challenges and Limitations

JavaScript's flexibility can produce implicit conversions, mutable shared state, and runtime errors. TypeScript reduces many errors but cannot prove that external data is valid and cannot prevent all logical or design defects. Asynchronous code introduces loading races, cancellation concerns, and partial failure. Browser differences, network unreliability, accessibility requirements, and security threats must be considered in professional applications.

A reliable developer therefore combines language knowledge with defensive boundaries: validate inputs, model states explicitly, handle failure, minimise side effects, test core logic, and observe real behaviour in supported browsers.

Chapter Summary

JavaScript provides the executable logic of frontend applications. const and let support controlled variable management; primitive and reference values explain assignment behaviour; strict equality and deliberate conversion reduce surprises. Conditions, loops, functions, closures, arrays, objects, maps, and sets provide the basic tools for computation and state management. ES modules divide programs into coherent files, while promises and async/await support network and event-driven work.

Reliable interface logic validates external data, checks HTTP failures, separates calculation from presentation, and handles errors at meaningful boundaries. TypeScript adds compile-time descriptions through inference, interfaces, unions, narrowing, generics, and strict null checking. It improves maintainability but does not replace runtime validation. Professional practice also requires accessibility, security, testing, dependency awareness, and critical review of AI-generated code.

Key Terms

Variable: A named binding that refers to a value.

Primitive: An immutable non-object value such as a string, number, or boolean.

Reference: A value used to locate an object, array, or function.

Scope: The region of a program in which a name can be accessed.

Closure: A function together with access to its surrounding lexical environment.

Array: An ordered, index-based collection.

Object: A collection of named properties.

Map: A collection of key–value pairs supporting keys of any type.

Set: A collection containing unique values.

Module: A file with explicit imports and exports.

Promise: An object representing the eventual result of asynchronous work.

Exception: An abnormal condition signalled with throw.

Type inference: The compiler's deduction of a value's type.

Union type: A type allowing a value from several specified alternatives.

Type narrowing: Refining a broad type using runtime checks.

Generic: A parameterised type or function that preserves relationships among types.

Runtime validation: Checking actual data while a program executes.

Immutability: A practice of creating changed values rather than altering existing values.

Review Questions

Short Answer Questions

  1. Why is const generally preferred over let and var?
  2. What is the difference between === and ==?
  3. When should Map be preferred to a plain object?
  4. Why must a program check response.ok after using fetch?
  5. What problem does TypeScript's unknown type help address?

Long Answer Questions

  1. Explain primitive and reference values, using an example in which two variables refer to the same object.
  2. Compare map, filter, find, and reduce, and describe an appropriate use for each.
  3. Explain lexical scope and closures and discuss one benefit and one risk of closures.
  4. Describe how modules and separation of concerns improve a frontend project.
  5. Explain why TypeScript improves reliability but cannot replace runtime validation.

Analytical / Application Questions

  1. Design the validation and error-handling flow for a registration form that submits asynchronously. Identify client-side and server-side responsibilities.
  2. A cart stores prices as decimal JavaScript numbers and occasionally displays a total that is one cent different. Diagnose the cause and propose a safer representation.
  3. Refactor a single function that reads DOM input, calculates a total, sends a request, and displays an error. Explain the modules or functions you would create and the tests each should receive.

Multiple Choice Questions with Answers

Question 1

Which declaration is appropriate when a variable binding should not be reassigned?

A. var

B. const

C. let

D. static

Correct Answer: B. const

Question 2

What does the expression 0 ?? 25 evaluate to?

A. 25

B. undefined

C. 0

D. false

Correct Answer: C. 0

Question 3

Which array method creates a new array by transforming every element?

A. map

B. find

C. some

D. reduce

Correct Answer: A. map

Question 4

Why should numeric arrays usually be sorted with (a, b) => a - b?

A. It prevents duplicate values

B. The default sort compares values numerically

C. The default sort compares string representations

D. It converts every value to a Boolean

Correct Answer: C. The default sort compares string representations

Question 5

What does fetch normally do when the server returns HTTP 404?

A. It always rejects automatically

B. It resolves, so code should inspect response.ok

C. It returns null without a response

D. It retries indefinitely

Correct Answer: B. It resolves, so code should inspect response.ok

Question 6

Which TypeScript type requires checking before using a value?

A. any

B. unknown

C. never

D. void

Correct Answer: B. unknown

Question 7

What is the principal purpose of a TypeScript interface?

A. To validate JSON at runtime automatically

B. To describe the expected structure of values during type checking

C. To compile CSS into JavaScript

D. To create a database table

Correct Answer: B. To describe the expected structure of values during type checking

Question 8

Which collection guarantees unique stored values?

A. Array

B. Object

C. Set

D. String

Correct Answer: C. Set

Question 9

Why are currency amounts often stored as integer cents?

A. Integers eliminate all server errors

B. They avoid many decimal floating-point representation problems

C. They automatically apply tax

D. They make values display as currency without formatting

Correct Answer: B. They avoid many decimal floating-point representation problems

Question 10

What does an async function return?

A. A Promise

B. Only a string

C. A synchronous iterator

D. A DOM element

Correct Answer: A. A Promise

Frontend Development
Chapter 4 of 15