# JavaScript & TypeScript Interview Questions (2025)

Source: https://www.techinterview.org/post/3233460385/javascript-typescript-interview-questions/
Updated: 2026-04-16 · techinterview.org

JavaScript drives nearly every web frontend and an increasing share of backend (Node.js). TypeScript has become the default for serious JS projects. This guide covers the most common JS/TS interview questions with working code examples.

## Core JavaScript Concepts

### 1. How does the JavaScript Event Loop work?


```
/**
 * JavaScript is single-threaded — one call stack.
 * The Event Loop coordinates async work:
 *
 *  Call Stack
 *  ┌──────────────┐
 *  │  setTimeout  │ → Web API/Node timer → Callback Queue
 *  │  fetch()     │ → Web API/libuv → Microtask Queue (Promise)
 *  └──────────────┘
 *
 * Priority: Call Stack → Microtask Queue (Promises) → Macrotask Queue (setTimeout, I/O)
 *
 * Microtasks run after EVERY task, draining completely before next macrotask.
 */

console.log("1 - sync");

setTimeout(() => console.log("4 - setTimeout (macrotask)"), 0);

Promise.resolve()
  .then(() => console.log("2 - promise microtask"))
  .then(() => console.log("3 - chained microtask"));

console.log("1.5 - still sync");

// Output order: 1, 1.5, 2, 3, 4
// Explanation: sync runs first, then microtasks flush completely, then macrotask
```


### 2. Closures — how and why they work


```
// A closure is a function that retains access to its lexical scope
// even after the outer function has returned.

function makeCounter(initial = 0) {
  let count = initial;  // Closed-over variable
  return {
    increment: () => ++count,
    decrement: () => --count,
    reset:     () => { count = initial; },
    value:     () => count,
  };
}

const c = makeCounter(10);
c.increment(); // 11
c.increment(); // 12
c.decrement(); // 11
c.reset();     // 10

// Classic bug: closure in loops
const badFns = [];
for (var i = 0; i < 3; i++) {
  badFns.push(() => i);  // All capture same `i` (var is function-scoped)
}
console.log(badFns.map(f => f())); // [3, 3, 3] — wrong!

// Fix 1: use let (block-scoped — creates new binding each iteration)
const goodFns = [];
for (let i = 0; i < 3; i++) {
  goodFns.push(() => i);
}
console.log(goodFns.map(f => f())); // [0, 1, 2]

// Fix 2: IIFE to capture value
const iifeFns = [];
for (var i = 0; i < 3; i++) {
  iifeFns.push(((n) => () => n)(i));
}
console.log(iifeFns.map(f => f())); // [0, 1, 2]

// Practical use: memoization
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn.apply(this, args));
    }
    return cache.get(key);
  };
}
```


### 3. Prototypal Inheritance and the `this` keyword


```
// JavaScript uses prototype chain for inheritance
// Every object has [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf)

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a noise.`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  speak() {
    return `${this.name} barks.`;
  }
}

// Under the hood, classes are syntactic sugar over prototypes:
// Dog.prototype.__proto__ === Animal.prototype
// new Dog("Rex").speak() → looks up Dog.prototype.speak first

// `this` binding rules (in priority order):
// 1. new binding:      new Foo()        → `this` = new object
// 2. Explicit binding: foo.call(ctx)    → `this` = ctx
// 3. Implicit binding: obj.foo()        → `this` = obj
// 4. Default binding:  foo()            → `this` = global (undefined in strict mode)
// Arrow functions:     no own `this`   → inherits from lexical scope

const obj = {
  name: "Alice",
  greet: function() { return `Hi, ${this.name}`; },
  greetArrow: () => `Hi, ${this?.name}`,  // `this` is outer scope (likely global)
};

const greet = obj.greet;
greet();            // "Hi, undefined" — lost binding (default binding)
obj.greet();        // "Hi, Alice" — implicit binding
greet.call(obj);    // "Hi, Alice" — explicit binding
greet.bind(obj)();  // "Hi, Alice" — bound function
```


### 4. Promises and async/await


```
// Promise states: pending → fulfilled | rejected (terminal states)

// Sequential vs parallel async:
async function sequential() {
  const a = await fetch("/api/a");  // Wait for a
  const b = await fetch("/api/b");  // Then wait for b
  // Total time: latency_a + latency_b
}

async function parallel() {
  const [a, b] = await Promise.all([fetch("/api/a"), fetch("/api/b")]);
  // Total time: max(latency_a, latency_b)
}

// Promise combinators:
async function robustFetch(urls) {
  // allSettled: never rejects — gets results OR errors for all
  const results = await Promise.allSettled(urls.map(u => fetch(u)));
  return results.map(r => r.status === "fulfilled" ? r.value : null);
}

async function race(urls) {
  // race: first to resolve/reject wins
  return Promise.race(urls.map(u => fetch(u)));
}

async function firstSuccess(urls) {
  // any: first to FULFILL (ignores rejections unless all fail)
  return Promise.any(urls.map(u => fetch(u)));
}

// Retry with exponential backoff:
async function withRetry(fn, maxAttempts = 3, baseDelayMs = 500) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts - 1) throw err;
      await new Promise(r => setTimeout(r, baseDelayMs * 2 ** attempt));
    }
  }
}
```


### 5. TypeScript Generics and Advanced Types


```
// Generics enable type-safe reusable code

function identity(arg: T): T { return arg; }
const num = identity(42);    // T inferred as number
const str = identity("hi");  // T inferred as string

// Constrained generics
function getProperty(obj: T, key: K): T[K] {
  return obj[key];
}
const name = getProperty({name: "Alice", age: 30}, "name");  // string

// Utility types
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

type UpdateUserDTO  = Partial<Pick>;   // optional subset
type PublicUser     = Omit;                     // exclude field
type ReadonlyUser   = Readonly;                          // all readonly
type UserRecord     = Record;                    // index signature

// Conditional types
type NonNullable = T extends null | undefined ? never : T;
type Awaited     = T extends Promise ? R : T;
type ReturnType  = T extends (...args: any[]) => infer R ? R : never;

// Discriminated unions — exhaustive type narrowing
type Shape =
  | { kind: "circle";    radius: number }
  | { kind: "square";    side: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":    return Math.PI * shape.radius ** 2;
    case "square":    return shape.side ** 2;
    case "rectangle": return shape.width * shape.height;
    default:
      // TypeScript exhaustiveness check:
      const _exhaustive: never = shape;
      throw new Error(`Unknown shape: ${_exhaustive}`);
  }
}
```


### 6. Common Array/Object Patterns


```
// Immutable data patterns (React/Redux best practices)
const state = { user: { name: "Alice" }, items: [1, 2, 3] };

// Shallow copy — works for flat updates
const nextState = { ...state, items: [...state.items, 4] };

// Deep merge — structuredClone (modern, built-in)
const deep = structuredClone(state);
deep.user.name = "Bob";  // Does not mutate original

// Array transformations
const products = [
  { id: 1, category: "a", price: 10 },
  { id: 2, category: "b", price: 20 },
  { id: 3, category: "a", price: 15 },
];

// Group by category
const grouped = products.reduce((acc, p) => {
  (acc[p.category] ??= []).push(p);
  return acc;
}, {} as Record);

// Sort by price desc, then id asc
const sorted = [...products].sort((a, b) =>
  b.price - a.price || a.id - b.id
);

// Pipe / compose
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const double = x => x * 2;
const addTen = x => x + 10;
const process = pipe(double, addTen);
console.log(process(5)); // (5*2)+10 = 20
```


## Runtime Complexity of Built-in Operations

| Operation | Complexity | Notes |
| --- | --- | --- |
| Array push/pop | O(1) amortized | Dynamic array |
| Array shift/unshift | O(n) | Shifts all elements |
| Array indexOf/find | O(n) | Linear scan |
| Map/Set get/has/set | O(1) average | Hash table |
| Object property access | O(1) average | V8 hidden classes optimize |
| Spread/Array.from | O(n) | Copies elements |
| JSON.stringify/parse | O(n) | n = total node count |

## Frequently Asked Questions

### How does the JavaScript event loop work?

JavaScript is single-threaded. The event loop coordinates asynchronous work: the call stack runs synchronous code, microtasks (Promise callbacks) are processed after each task draining completely, then macrotasks (setTimeout, I/O callbacks) are processed one per loop iteration. Microtasks always run before the next macrotask.

### What is a closure in JavaScript?

A closure is a function that retains access to variables from its outer (enclosing) scope even after the outer function has returned. Closures are created every time a function is created in JavaScript. They enable patterns like module pattern (private variables), factory functions, memoization, and partial application.

### What is the difference between == and === in JavaScript?

=== (strict equality) compares both value and type — no coercion. == (loose equality) coerces types before comparing, leading to surprising results like null == undefined being true, or 0 == "" being true. Always use === unless you specifically want type coercion, which is rarely the right choice.
