JavaScript & TypeScript Interview Questions (2025)

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  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  i);
}
console.log(goodFns.map(f => f())); // [0, 1, 2]

// Fix 2: IIFE to capture value
const iifeFns = [];
for (var i = 0; i  () => 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  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.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does the JavaScript event loop work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “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.”
}
},
{
“@type”: “Question”,
“name”: “What is a closure in JavaScript?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “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.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between == and === in JavaScript?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “=== (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.”
}
}
]
}

Scroll to Top