Frontend Interview Prep: JavaScript & TypeScript

Frontend Interview Prep: JavaScript & TypeScript

> A comprehensive guide to JavaScript and TypeScript concepts commonly asked in frontend interviews.

🧠 Take the Interactive Quiz β†’

Test your knowledge with the interactive quiz! Multiple choice questions covering JS gotchas, TypeScript, React, and Node.js.


Table of Contents

  • Quick Reference
  • React Gotchas
  • - Reconciliation & Virtual DOM

    - setState Batching & Stale State

    - Props vs State

    - Controlled Components

    - Lifting State Up

    - Mutating State Reference

    - useEffect Dependencies

    - useEffect Cleanup

    - Keys in Lists

    - Rules of Hooks

    - Async in useEffect

  • JavaScript Gotchas
  • - [[] == ![]](#---)

    - Arrow Function Block Body

    - typeof null

    - typeof Array

    - Empty Array Addition

    - Event Loop

    - Spread and Shallow Copy

    - this in Arrow Functions

    - Timeout with var

    - Closures

    - Object Reference Assignment

    - Array.sort() Default Behavior

    - NaN Comparisons

    - Floating Point Precision

    - const with Objects

    - Temporal Dead Zone

    - parseInt Radix

    - Hoisting Differences

    - delete Keyword

  • Core JavaScript Concepts
  • - OOP in JavaScript

    - Data Types

    - Promises

    - Hoisting

    - this Binding

    - Prototype Chain

    - Event Bubbling and Capturing

    - Debounce and Throttle

    - Error Handling

    - Optional Chaining

    - Arrow Functions

    - Single-Threaded Nature

    - Microtasks vs Macrotasks

    - Pass by Value vs Reference

    - Equality Operators

    - Nullish Coalescing

  • Common Methods Quick Reference
  • - Array Methods

    - String Methods

    - Object Methods

    - Promise Methods

    - Set & Map

  • Array Methods
  • TypeScript Essentials
  • - All TypeScript Types

    - Variable Declarations

    - Destructuring

    - Interfaces

    - Composing Types

    - Type vs Interface

  • TypeScript Utility Types
  • - Omit

    - Record

    - Pick

    - Partial

    - Required

    - Readonly

    - ReturnType

    - Parameters

    - Type Guards

    - as const Assertions

  • Node.js & Package Management
  • - What is Node.js?

    - npm vs npx vs yarn vs pnpm

    - package.json

    - Dependencies vs DevDependencies

    - Semantic Versioning

    - package-lock.json

    - node_modules

    - Common npm Commands

    - ES Modules vs CommonJS

    - Environment Variables

  • React
  • - The React Mental Model

    - State In-Depth

    - Props In-Depth

    - Re-rendering In-Depth

    - What is React?

    - JSX

    - Components

    - Props

    - useState

    - useEffect

    - useRef

    - useMemo and useCallback

    - useContext

    - Custom Hooks

    - Controlled vs Uncontrolled

    - Keys in Lists

    - Virtual DOM

    - Component Lifecycle

    - React.memo

    - Fragments

    - Higher-Order Components

    - Error Boundaries

    - Suspense and Lazy Loading

    - Portals

    - Strict Mode

    - Common Interview Questions


    Quick Reference

    | Command | Description |

    |---------|-------------|

    | npm install | Install a package (e.g., npm create vite@latest) |

    | npx | Run a command (e.g., npx create-react-app .) |


    React Gotchas

    Reconciliation & Virtual DOM

    React uses a Virtual DOM β€” a lightweight in-memory representation of the actual DOM.

    How it works:

    
    State/Props change
           ↓
    React creates NEW virtual DOM tree
           ↓
    Diffing: Compare new vs old virtual DOM
           ↓
    Calculate minimal set of changes
           ↓
    Batch update real DOM (reconciliation)
    

    Why? Direct DOM manipulation is expensive. React's diffing algorithm compares virtual DOM trees and updates only what changed.

    | Concept | Description |

    |---------|-------------|

    | Virtual DOM | JavaScript object mirroring DOM structure |

    | Diffing | Algorithm comparing two virtual DOM trees |

    | Reconciliation | Process of updating real DOM with minimal changes |

    > Key insight: React assumes elements with the same key are the same element. That's why stable keys matter for lists!


    setState Batching & Stale State

    Problem: Multiple setState calls using the current state value don't accumulate.

    
    const [items, setItems] = useState([])
    
    function addItem(item) {
      setItems([...items, item])  // items = []
      setItems([...items, item])  // items = [] (still!)
    }
    // Result: Only ONE item added, not two!
    

    Why? Inside a single render, items is a snapshot β€” it doesn't change mid-function. Both calls use the same stale [] value.

    | What Happens | Value of items |

    |--------------|------------------|

    | First setItems | [] β†’ schedules [item] |

    | Second setItems | [] β†’ schedules [item] (overwrites!) |

    | After re-render | [item] (only one) |

    Solution: Use functional updates to access the latest state:

    
    function addItem(item) {
      setItems(prev => [...prev, item])  // prev = []
      setItems(prev => [...prev, item])  // prev = [item] βœ“
    }
    // Result: TWO items added correctly!
    

    > Rule: When new state depends on previous state, always use setState(prev => newValue).


    Props vs State

    | Aspect | Props | State |

    |--------|-------|-------|

    | Source | Passed from parent | Managed inside component |

    | Mutability | Read-only (immutable) | Can change via setState |

    | On Change | Parent re-renders child | Component re-renders |

    | Ownership | Parent owns | Component owns |

    
    // Props: received from parent
    function Greeting({ name }) {        // ← props
      return <h1>Hello, {name}!</h1>
    }
    
    // State: managed internally  
    function Counter() {
      const [count, setCount] = useState(0)  // ← state
      return <button onClick={() => setCount(c => c + 1)}>{count}</button>
    }
    

    > Key insight: Props flow down, events flow up. State triggers re-renders when it changes.


    Controlled Components

    A controlled component is a form input whose value is controlled by React state.

    
    const [species, setSpecies] = useState("")
    
    <input
      value={species}                          // ← value FROM state
      onChange={(e) => setSpecies(e.target.value)}  // ← updates state
    />
    

    Data Flow:

    
    User types β†’ onChange fires β†’ setState updates β†’ React re-renders β†’ input shows new value
    

    | Type | Source of Truth | Example |

    |------|-----------------|---------|

    | Controlled | React state | |

    | Uncontrolled | DOM | |

    > Best practice: Use controlled components for form validation, conditional disabling, and enforcing input formats.


    Lifting State Up

    Question: Why does state live in App and not in child components like BatLogForm or ActivityTable?

    Answer: Both components need access to the same data.

    React uses unidirectional (top-down) data flow:

  • State lives in the nearest common ancestor
  • Data flows down via props
  • Events flow up via callbacks
  • 
    BatLogForm ──callback──→ App (state) ──props──→ ActivityTable
    

    This pattern is called "lifting state up" β€” move state to the component that needs to share it with others.


    Mutating State Reference

    Why will this not make React re-render the component?

    
    logs.push("D")
    setLogs(logs)
    

    Because react does not care about the content, it cares about references.

    Internally, react does this:

    
    if (oldState === newState)
        skip render
    else
        render
    

    So when you do this

    
    setLogs(logs)
    

    Logs is still the same array object in memory. You mutated it, but the reference didn't change.


    useEffect Dependencies

    Question: What's the difference between these?

    
    // 1. No dependency array
    useEffect(() => {
      console.log("runs")
    })
    
    // 2. Empty dependency array
    useEffect(() => {
      console.log("runs")
    }, [])
    
    // 3. With dependencies
    useEffect(() => {
      console.log("runs")
    }, [count])
    

    Answer:

    | Syntax | When it runs |

    |--------|--------------|

    | No array | Every render |

    | [] | Mount only (once) |

    | [count] | Mount + when count changes |

    Common mistake: Forgetting dependencies causes stale closures:

    
    // ❌ Bug: always logs initial count
    useEffect(() => {
      setInterval(() => console.log(count), 1000)
    }, [])  // count is stale!
    
    // βœ… Fix: include count in deps
    useEffect(() => {
      const id = setInterval(() => console.log(count), 1000)
      return () => clearInterval(id)
    }, [count])
    

    useEffect Cleanup

    Question: When does the cleanup function run?

    
    useEffect(() => {
      const subscription = subscribe()
      
      return () => {
        subscription.unsubscribe()  // Cleanup
      }
    }, [])
    

    Answer:

  • On unmount (component removed from DOM)
  • Before the next effect runs (if deps changed)
  • Why cleanup matters:

    
    // ❌ Memory leak - timer keeps running after unmount
    useEffect(() => {
      setInterval(() => setCount(c => c + 1), 1000)
    }, [])
    
    // βœ… Proper cleanup
    useEffect(() => {
      const id = setInterval(() => setCount(c => c + 1), 1000)
      return () => clearInterval(id)
    }, [])
    

    Keys in Lists

    Question: Why is using array index as key problematic?

    
    // ❌ Bad: index as key
    {items.map((item, index) => (
      <Item key={index} data={item} />
    ))}
    
    // βœ… Good: unique stable ID
    {items.map(item => (
      <Item key={item.id} data={item} />
    ))}
    

    Answer: When items are reordered/removed, React matches by key. Index-based keys cause:

    | Problem | What Happens |

    |---------|--------------|

    | Wrong component reused | State gets mixed up |

    | Incorrect animations | Wrong elements animate |

    | Input values lost | Focus jumps, values reset |

    Rule: Keys should be stable, unique, and derived from data, not position.


    Rules of Hooks

    Question: What's wrong with this code?

    
    function Component({ showExtra }) {
      const [count, setCount] = useState(0)
      
      if (showExtra) {
        const [extra, setExtra] = useState('')  // ❌ ERROR!
      }
      
      return <div>{count}</div>
    }
    

    Answer: Hooks called conditionally breaks React!

    Rules of Hooks:

    1. βœ… Only call hooks at the top level (not in loops, conditions, nested functions)

    2. βœ… Only call hooks from React functions (components or custom hooks)

    Why? React relies on hook call order to track state. Conditional hooks change the order.

    Fix:

    
    function Component({ showExtra }) {
      const [count, setCount] = useState(0)
      const [extra, setExtra] = useState('')  // Always called
      
      // Use the value conditionally instead
      return (
        <div>
          {count}
          {showExtra && <input value={extra} onChange={...} />}
        </div>
      )
    }
    

    Async in useEffect

    Question: Why doesn't this work?

    
    // ❌ Wrong
    useEffect(async () => {
      const data = await fetchData()
      setData(data)
    }, [])
    

    Answer: useEffect expects the callback to return nothing or a cleanup function. async functions return a Promise.

    Fix: Define the async function inside:

    
    // βœ… Correct
    useEffect(() => {
      async function loadData() {
        const data = await fetchData()
        setData(data)
      }
      loadData()
    }, [])
    
    // βœ… Or use IIFE
    useEffect(() => {
      (async () => {
        const data = await fetchData()
        setData(data)
      })()
    }, [])
    

    JavaScript Gotchas

    [] == ![]

    What is the output?

    
    console.log([] == ![])
    

    Surprisingly, it is true. In JavaScript, all objects are truthy, including arrays. So ![] is the same as !true. Then coercion happens. And now [] == !true. Now [] is considered 0. So 0 == false. And that is true. JS is insane.

    Arrow Function Block Body

    Question: What does this output?

    
    const nums = [1, 2, 3]
    
    const result = nums.map(n => {
      n * 2
    })
    
    console.log(result)
    

    Answer: [undefined, undefined, undefined] (not [2, 4, 6]!)

    Why? Arrow functions with {} have a block body, not an expression body. You must explicitly return a value.

    
    // ❌ Block body without return
    n => {
      n * 2
    }
    
    // βœ… Block body with return
    n => {
      return n * 2
    }
    
    // βœ… Expression body (implicit return)
    n => n * 2
    

    typeof null

    Question: What does this return?

    
    console.log(typeof null)
    

    Answer: "object"

    Why? This is a historical bug in JavaScript that was never fixed for backward compatibility. null is a primitive, but typeof null returns "object".


    typeof Array

    Question: What does this return?

    
    console.log(typeof [])
    

    Answer: "object" (not "array"!)

    Why? Arrays are objects in JavaScript. Use Array.isArray([]) to check for arrays.


    Empty Array Addition

    Question: What does this return?

    
    console.log([] + [])
    

    Answer: ""

    Why? The + operator tries to convert arrays to primitives. Arrays convert to strings via .toString(), which returns "" for empty arrays. So "" + "" equals "".


    Event Loop

    Question: What is the output?

    
    console.log("A")
    
    setTimeout(() => {
      console.log("B")
    }, 0)
    
    console.log("C")
    

    Answer: A C B

    Explanation: The setTimeout callback goes into the event loop and executes last, even with a 0ms delay.


    Spread and Shallow Copy

    Question: What does this output?

    
    const user = {
      name: "Ian",
      skills: ["JS"]
    }
    
    const copy = { ...user }
    copy.skills.push("React")
    
    console.log(user.skills)
    

    Answer: ["JS", "React"]

    Why? Spread (...) creates a shallow copy. Nested objects/arrays are still references to the original.

    | Level | Copied? |

    |-------|--------|

    | Top-level primitives | βœ… Yes (by value) |

    | Nested objects/arrays | ❌ No (same reference) |

    Fix: Deep clone for nested objects:

    
    // Using structuredClone (modern)
    const deepCopy = structuredClone(user)
    
    // Using JSON (has limitations)
    const deepCopy = JSON.parse(JSON.stringify(user))
    

    this in Arrow Functions

    
    const obj = {
      value: 10,
      print: () => {
        console.log(this.value)
      }
    }
    
    obj.print() // undefined
    

    Why? Arrow functions capture this from their surrounding lexical scope (global scope here), which has no value property.

    Key Point: Arrow functions do NOT have their own this. They inherit it from the enclosing scope.

    When Arrow Functions Work for this

    
    const user = {
      name: "Ian",
      greet() {
        // Arrow function inherits `this` from greet()
        return () => {
          console.log(this.name) // "Ian"
        }
      }
    }
    
    const fn = user.greet()
    fn() // Works! Prints "Ian"
    

    Fix: Use a regular function:

    
    const obj = {
      value: 10,
      print() {
        console.log(this.value)
      }
    }
    
    obj.print() // 10
    

    Timeout with var

    Question: What will this output?

    
    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0)
    }
    

    Answer: 3 3 3 (not 2 2 2!)

    Why this happens:

    1. var is function-scoped, not block-scoped

    2. setTimeout runs after the loop finishes (event loop)

    3. When callbacks execute, i === 3 (loop ended at 3 < 3 === false)

    Fix: Use let (creates new i each iteration):

    
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0)
    }
    // Output: 0 1 2
    

    Closures

    
    function outer() {
      let count = 0
    
      return function() {
        count++
        return count
      }
    }
    
    const fn = outer()
    
    console.log(fn()) // 1
    console.log(fn()) // 2
    console.log(fn()) // 3
    

    Key Concept: The inner function remembers variables from its surrounding scope. This is a closure β€” the returned function retains access to count even after outer() finishes.


    Object Reference Assignment

    
    let a = { value: 0 }
    let b = a
    b.value = 5
    
    console.log(a.value) // 5
    

    Why? JavaScript doesn't copy objects β€” it copies the reference. Both a and b point to the same object in memory.


    Array.sort() Default Behavior

    Question: What does this output?

    
    const nums = [10, 5, 40, 25, 100]
    console.log(nums.sort())
    

    Answer: [10, 100, 25, 40, 5] (not [5, 10, 25, 40, 100]!)

    Why? sort() converts elements to strings and sorts lexicographically by default.

    Fix: Always provide a compare function for numbers:

    
    nums.sort((a, b) => a - b)  // Ascending: [5, 10, 25, 40, 100]
    nums.sort((a, b) => b - a)  // Descending: [100, 40, 25, 10, 5]
    

    NaN Comparisons

    Question: What does this output?

    
    console.log(NaN === NaN)
    console.log(NaN == NaN)
    

    Answer: Both are false!

    Why? NaN is the only JavaScript value not equal to itself. This is by IEEE 754 floating-point spec.

    How to check for NaN:

    
    // ❌ Don't use
    x === NaN  // Always false
    
    // βœ… Use these
    Number.isNaN(x)     // Recommended (strict)
    isNaN(x)            // Coerces to number first
    Object.is(x, NaN)   // Also works
    

    Floating Point Precision

    Question: What does this output?

    
    console.log(0.1 + 0.2 === 0.3)
    

    Answer: false!

    Why? 0.1 + 0.2 equals 0.30000000000000004 due to binary floating-point representation.

    Fix: Compare with tolerance:

    
    const isEqual = Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON
    

    const with Objects

    Question: Does this throw an error?

    
    const user = { name: "Ian" }
    user.name = "John"
    

    Answer: No! It works fine.

    Why? const prevents reassignment, not mutation. The binding is constant, but the object's properties can change.

    
    const user = { name: "Ian" }
    user.name = "John"     // βœ… OK - mutating property
    user = { name: "Bob" } // ❌ TypeError - reassigning const
    

    To make object immutable:

    
    const user = Object.freeze({ name: "Ian" })
    user.name = "John"  // Silently fails (or throws in strict mode)
    

    Temporal Dead Zone (TDZ)

    Question: What does this output?

    
    console.log(x)
    let x = 5
    

    Answer: ReferenceError: Cannot access 'x' before initialization

    Why? let and const are hoisted but not initialized. The time between entering scope and declaration is the Temporal Dead Zone.

    | Declaration | Hoisted? | Initialized? | TDZ? |

    |-------------|----------|--------------|------|

    | var | βœ… | βœ… (as undefined) | ❌ |

    | let | βœ… | ❌ | βœ… |

    | const | βœ… | ❌ | βœ… |

    | function | βœ… | βœ… (full function) | ❌ |


    parseInt Radix

    Question: What does this output?

    
    console.log(parseInt("08"))
    console.log(parseInt("08", 10))
    

    Answer: Both return 8 in modern browsers, but historically parseInt("08") could return 0 (octal parsing).

    Best practice: Always specify the radix:

    
    parseInt("08", 10)   // 8 - explicitly base 10
    parseInt("1010", 2)  // 10 - binary
    parseInt("ff", 16)   // 255 - hexadecimal
    

    Hoisting Differences

    Question: What's the difference?

    
    // Function Declaration - fully hoisted
    sayHi()  // βœ… Works!
    function sayHi() { console.log("Hi") }
    
    // Function Expression - only variable hoisted
    sayBye() // ❌ TypeError: sayBye is not a function
    var sayBye = function() { console.log("Bye") }
    

    Summary:

    | Type | Hoisted | Usable Before Declaration |

    |------|---------|---------------------------|

    | Function Declaration | βœ… Whole function | βœ… Yes |

    | Function Expression (var) | βœ… Variable only (as undefined) | ❌ No |

    | Function Expression (let/const) | βœ… Variable only | ❌ No (TDZ) |

    | Arrow Function | Same as expression | ❌ No |


    delete Keyword

    Question: What does this output?

    
    const obj = { a: 1 }
    console.log(delete obj.a)
    console.log(obj.a)
    
    let x = 5
    console.log(delete x)
    

    Answer: true, undefined, false

    Why?

  • delete removes object properties and returns true
  • delete on variables returns false (doesn't work)
  • After deletion, accessing the property returns undefined

  • Core JavaScript Concepts

    OOP in JavaScript

    Basic Class Syntax

    
    class User {
      constructor(name) {
        this.name = name
      }
    
      greet() {
        return "Hello " + this.name
      }
    }
    
    const u = new User("Ian")
    

    Inheritance

    
    class Animal {
      speak() {
        console.log("sound")
      }
    }
    
    class Dog extends Animal {
      speak() {
        console.log("bark")
      }
    }
    

    super Keyword

    Calls the parent class constructor or methods:

    
    class Animal {
      constructor(name) {
        this.name = name
      }
    }
    
    class Dog extends Animal {
      constructor(name) {
        super(name) // Call parent constructor
      }
    }
    

    Encapsulation (Private Fields)

    Use # prefix for private fields:

    
    class Counter {
      #count = 0
    
      inc() {
        this.#count++
      }
    
      get value() {
        return this.#count
      }
    }
    

    Data Types

    Primitives

    | Type | Example |

    |------|------|

    | string | "hello" |

    | number | 42 |

    | boolean | true |

    | undefined | undefined |

    | null | null |

    | symbol | Symbol("id") |

    | bigint | 123n |

    Reference Types (Objects)

    | Type | Example |

    |------|------|

    | Object | { name: "Ian" } |

    | Array | [1, 2, 3] |

    | Function | function() {} |

    | Date | new Date() |

    | Map | new Map() |

    | Set | new Set() |

    | Promise | new Promise(...) |


    Promises

    A Promise represents the result of an asynchronous operation.

    States

    | State | Description |

    |-------|-------------|

    | pending | Initial state, operation in progress |

    | fulfilled | Operation completed successfully |

    | rejected | Operation failed |

    Creating a Promise

    
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("done")
      }, 1000)
    })
    

    Consuming with .then()

    
    promise.then(result => {
      console.log(result)
    })
    

    Modern async/await Syntax

    
    async function run() {
      const result = await promise
      console.log(result)
    }
    

    Real-World Example

    
    const res = await fetch("/api/users")
    const data = await res.json()
    

    Chaining

    
    Promise.resolve(5)
      .then(x => x * 2)
      .then(console.log) // 10
    

    Hoisting

    Hoisting moves declarations to the top of their scope during compilation.

    
    console.log(x) // undefined (not ReferenceError)
    var x = 5
    
    console.log(y) // ReferenceError: Cannot access 'y' before initialization
    let y = 5
    

    | Declaration | Hoisted? | Initialized? |

    |-------------|----------|-------------|

    | var | βœ… | As undefined |

    | let / const | βœ… | ❌ (TDZ) |

    | function | βœ… | βœ… (fully) |

    | class | βœ… | ❌ (TDZ) |

    Temporal Dead Zone (TDZ): The period between entering a scope and the variable being declared. Accessing let/const in TDZ throws an error.


    this Binding

    this depends on how a function is called, not where it's defined.

    | Context | this Value |

    |---------|-------------|

    | Global (non-strict) | window / global |

    | Global (strict) | undefined |

    | Object method | The object |

    | Arrow function | Lexical (inherited) |

    | new constructor | The new instance |

    | call/apply/bind | Explicitly set |

    call, apply, bind

    
    function greet(greeting) {
      return `${greeting}, ${this.name}`
    }
    
    const user = { name: "Ian" }
    
    // call - invoke immediately, args as list
    greet.call(user, "Hello")        // "Hello, Ian"
    
    // apply - invoke immediately, args as array
    greet.apply(user, ["Hi"])        // "Hi, Ian"
    
    // bind - returns new function with bound `this`
    const boundGreet = greet.bind(user)
    boundGreet("Hey")                // "Hey, Ian"
    

    Prototype Chain

    Every object has a hidden [[Prototype]] link to another object.

    
    const animal = { eats: true }
    const dog = Object.create(animal)
    dog.barks = true
    
    console.log(dog.eats)   // true (inherited from animal)
    console.log(dog.barks)  // true (own property)
    

    Prototype Chain:

    
    dog -> animal -> Object.prototype -> null
    

    Interview Question: "How does inheritance work in JavaScript?"

  • JS uses prototypal inheritance, not classical
  • Objects inherit directly from other objects
  • class syntax is just syntactic sugar over prototypes

  • Event Bubbling and Capturing

    When an event occurs on a nested element:

    1. Capturing phase: Event travels DOWN from window to target

    2. Target phase: Event reaches the target element

    3. Bubbling phase: Event travels UP from target to window

    
    // Bubbling (default)
    element.addEventListener('click', handler)
    
    // Capturing
    element.addEventListener('click', handler, true)
    // or
    element.addEventListener('click', handler, { capture: true })
    

    Event Delegation

    Attach one listener to a parent instead of many to children:

    
    // Instead of adding listener to each <li>
    document.querySelector('ul').addEventListener('click', (e) => {
      if (e.target.tagName === 'LI') {
        console.log('Clicked:', e.target.textContent)
      }
    })
    

    stopPropagation vs preventDefault

    | Method | Purpose |

    |--------|--------|

    | e.stopPropagation() | Stops event from bubbling/capturing further |

    | e.preventDefault() | Prevents default browser action (form submit, link navigation) |


    Debounce and Throttle

    Both limit how often a function executes.

    | Technique | Behavior | Use Case |

    |-----------|----------|----------|

    | Debounce | Wait until X ms of inactivity | Search input, resize |

    | Throttle | Execute at most once per X ms | Scroll, mousemove |

    Debounce Implementation

    
    function debounce(fn, delay) {
      let timeoutId
      return (...args) => {
        clearTimeout(timeoutId)
        timeoutId = setTimeout(() => fn(...args), delay)
      }
    }
    
    const search = debounce((query) => {
      console.log('Searching:', query)
    }, 300)
    

    Throttle Implementation

    
    function throttle(fn, limit) {
      let inThrottle
      return (...args) => {
        if (!inThrottle) {
          fn(...args)
          inThrottle = true
          setTimeout(() => inThrottle = false, limit)
        }
      }
    }
    

    Error Handling

    
    try {
      throw new Error("Something went wrong")
    } catch (error) {
      console.error(error.message)
    } finally {
      console.log("Always runs") // Cleanup code
    }
    

    Async Error Handling

    
    // With promises
    fetch('/api')
      .then(res => res.json())
      .catch(err => console.error(err))
    
    // With async/await
    async function fetchData() {
      try {
        const res = await fetch('/api')
        return await res.json()
      } catch (error) {
        console.error('Fetch failed:', error)
      }
    }
    

    Optional Chaining (?.)

    Safely access nested properties without checking each level:

    
    const user = { profile: { name: "Ian" } }
    
    // Without optional chaining
    const name = user && user.profile && user.profile.name
    
    // With optional chaining
    const name = user?.profile?.name // "Ian"
    
    // Works with methods and arrays
    user?.getAddress?.()
    users?.[0]?.name
    

    Arrow Functions

    These three are equivalent:

    
    // Traditional function
    function add(a, b) {
      return a + b
    }
    
    // Arrow function (implicit return)
    const add = (a, b) => a + b
    
    // Arrow function (explicit return)
    const add = (a, b) => {
      return a + b
    }
    

    Is JS Single-Threaded?

    Yes. JavaScript uses an event loop to handle asynchronous operations while remaining single-threaded.


    Microtasks and Macrotasks

    Microtasks run before macrotasks after the current call stack is empty.

    
    console.log("A")                                    // 1st
    setTimeout(() => console.log("B"), 0)               // 4th (macrotask)
    Promise.resolve().then(() => console.log("C"))      // 3rd (microtask)
    console.log("D")                                    // 2nd
    
    // Output: A D C B
    

    Are Objects Passed by Value or Reference?

    Objects are passed by value, but the value is a reference to the object.

    | Type | What's Copied |

    |------|---------------|

    | Primitives | The value itself |

    | Objects | A reference to the object |


    Equality Operators (== vs ===)

    | Operator | Name | Behavior |

    |----------|------|----------|

    | === | Strict equality | Compares value and type |

    | == | Loose equality | Coerces types before comparing |

    
    // Strict equality
    5 === 5              // true
    5 === "5"            // false
    null === undefined   // false
    
    // Loose equality (with coercion)
    5 == "5"             // true
    true == 1            // true
    null == undefined    // true
    

    Gotcha: Reference comparison

    
    [] === []  // false (different references)
    {} === {}  // false (different references)
    

    Nullish Coalescing

    
    const value = null
    console.log(value ?? "default") // "default"
    

    | Operator | Checks for |

    |----------|------------|

    | ?? | null or undefined only |

    | \|\| | All falsy values (false, 0, "", null, undefined, NaN) |


    Common Methods Quick Reference

    Quick syntax reference for frequently used JavaScript/TypeScript methods.

    Array Methods

    map - Transform each element

    
    const nums = [1, 2, 3]
    const doubled = nums.map(n => n * 2)
    // [2, 4, 6]
    
    // With index
    const indexed = nums.map((n, i) => `${i}: ${n}`)
    // ["0: 1", "1: 2", "2: 3"]
    

    filter - Keep matching elements

    
    const nums = [1, 2, 3, 4, 5]
    const even = nums.filter(n => n % 2 === 0)
    // [2, 4]
    
    // With type guard
    const strings = mixed.filter((x): x is string => typeof x === 'string')
    

    reduce - Accumulate to single value

    
    const nums = [1, 2, 3]
    // Sum
    const sum = nums.reduce((acc, n) => acc + n, 0) // 6
    
    // Object from array
    const users = [{id: 1, name: 'Ian'}, {id: 2, name: 'Ana'}]
    const byId = users.reduce((acc, u) => ({ ...acc, [u.id]: u }), {})
    // { 1: {id: 1, name: 'Ian'}, 2: {id: 2, name: 'Ana'} }
    
    // Group by
    const grouped = items.reduce((acc, item) => {
      const key = item.category
      acc[key] = [...(acc[key] || []), item]
      return acc
    }, {})
    

    find / findIndex - Get first match

    
    const users = [{id: 1, name: 'Ian'}, {id: 2, name: 'Ana'}]
    
    const user = users.find(u => u.id === 2)       // {id: 2, name: 'Ana'}
    const index = users.findIndex(u => u.id === 2) // 1
    

    some / every - Test conditions

    
    const nums = [1, 2, 3, 4, 5]
    
    nums.some(n => n > 3)  // true (at least one)
    nums.every(n => n > 0) // true (all match)
    

    includes - Check existence

    
    const nums = [1, 2, 3]
    nums.includes(2)  // true
    nums.includes(99) // false
    

    sort - Sort in place (mutates!)

    
    const nums = [3, 1, 2]
    
    // Numbers (default sort is alphabetical!)
    nums.sort((a, b) => a - b) // [1, 2, 3] ascending
    nums.sort((a, b) => b - a) // [3, 2, 1] descending
    
    // Strings
    names.sort((a, b) => a.localeCompare(b))
    
    // Objects
    users.sort((a, b) => a.age - b.age)
    

    slice - Copy portion (non-mutating)

    
    const arr = [1, 2, 3, 4, 5]
    arr.slice(1, 3)  // [2, 3] (from index 1 to 3, exclusive)
    arr.slice(-2)    // [4, 5] (last 2)
    arr.slice()      // [1, 2, 3, 4, 5] (shallow copy)
    

    splice - Remove/insert (mutates!)

    
    const arr = [1, 2, 3, 4, 5]
    arr.splice(2, 1)        // removes 1 element at index 2 β†’ arr is [1, 2, 4, 5]
    arr.splice(2, 0, 'new') // inserts 'new' at index 2
    arr.splice(1, 2, 'a', 'b') // replaces 2 elements starting at index 1
    

    flat / flatMap - Flatten nested arrays

    
    const nested = [[1, 2], [3, 4]]
    nested.flat()      // [1, 2, 3, 4]
    
    const nums = [1, 2, 3]
    nums.flatMap(n => [n, n * 2]) // [1, 2, 2, 4, 3, 6]
    

    concat / spread - Combine arrays

    
    const a = [1, 2]
    const b = [3, 4]
    
    a.concat(b)  // [1, 2, 3, 4]
    [...a, ...b] // [1, 2, 3, 4] (preferred)
    

    String Methods

    split / join

    
    'a,b,c'.split(',')        // ['a', 'b', 'c']
    ['a', 'b', 'c'].join('-') // 'a-b-c'
    

    substring / slice

    
    const str = 'Hello World'
    str.substring(0, 5) // 'Hello'
    str.slice(-5)       // 'World' (negative = from end)
    

    includes / startsWith / endsWith

    
    const str = 'Hello World'
    str.includes('World')    // true
    str.startsWith('Hello')  // true
    str.endsWith('World')    // true
    

    replace / replaceAll

    
    'foo bar foo'.replace('foo', 'baz')    // 'baz bar foo' (first only)
    'foo bar foo'.replaceAll('foo', 'baz') // 'baz bar baz' (all)
    'foo bar foo'.replace(/foo/g, 'baz')   // 'baz bar baz' (regex)
    

    trim / padStart / padEnd

    
    '  hello  '.trim()      // 'hello'
    '5'.padStart(3, '0')    // '005'
    '5'.padEnd(3, '0')      // '500'
    

    toUpperCase / toLowerCase

    
    'Hello'.toUpperCase() // 'HELLO'
    'Hello'.toLowerCase() // 'hello'
    

    Object Methods

    Object.keys / values / entries

    
    const user = { name: 'Ian', age: 30 }
    
    Object.keys(user)    // ['name', 'age']
    Object.values(user)  // ['Ian', 30]
    Object.entries(user) // [['name', 'Ian'], ['age', 30]]
    

    Object.fromEntries

    
    const entries = [['name', 'Ian'], ['age', 30]]
    Object.fromEntries(entries) // { name: 'Ian', age: 30 }
    
    // Transform object
    const doubled = Object.fromEntries(
      Object.entries(prices).map(([k, v]) => [k, v * 2])
    )
    

    Object.assign / spread

    
    // Merge objects (later wins)
    Object.assign({}, objA, objB)
    { ...objA, ...objB } // preferred
    
    // Shallow copy
    const copy = { ...original }
    

    Destructuring with defaults

    
    const { name, age = 25 } = user
    const { name: userName } = user // rename
    const { a, ...rest } = obj      // rest
    

    Promise Methods

    
    // Wait for all (fails if any fails)
    const results = await Promise.all([fetch(a), fetch(b), fetch(c)])
    
    // Wait for all (never fails, returns status)
    const results = await Promise.allSettled([p1, p2, p3])
    // [{status: 'fulfilled', value: ...}, {status: 'rejected', reason: ...}]
    
    // First to resolve
    const fastest = await Promise.race([p1, p2, p3])
    
    // First to succeed (ignores rejections)
    const first = await Promise.any([p1, p2, p3])
    

    Set & Map

    
    // Set - unique values
    const set = new Set([1, 2, 2, 3]) // {1, 2, 3}
    set.add(4)
    set.has(2)    // true
    set.delete(2)
    [...set]      // convert to array
    
    // Remove duplicates from array
    const unique = [...new Set(arr)]
    
    // Map - key-value pairs (any key type)
    const map = new Map()
    map.set('key', 'value')
    map.set(obj, 'works!')  // objects as keys!
    map.get('key')          // 'value'
    map.has('key')          // true
    map.delete('key')
    
    // Iterate
    for (const [key, value] of map) { }
    

    Array Iteration Methods

    for...of

    
    const numbers = [1, 2, 3, 4]
    
    for (const n of numbers) {
      console.log(n)
    }
    

    map

    
    const numbers = [1, 2, 3]
    const doubled = numbers.map(n => n * 2) // [2, 4, 6]
    

    reduce

    
    const numbers = [1, 2, 3]
    const sum = numbers.reduce((acc, n) => acc + n, 0) // 6
    

    forEach

    
    numbers.forEach(n => console.log(n))
    

    entries

    
    for (const [index, value] of numbers.entries()) {
      console.log(index, value)
    }
    

    find

    Returns the first matching element:

    
    const result = numbers.find(n => n > 2) // 3
    

    filter

    Returns all matching elements:

    
    const numbers = [1, 2, 3, 4, 5]
    const even = numbers.filter(n => n % 2 === 0) // [2, 4]
    

    Chaining: filter + map

    
    const result = users
      .filter(user => user.active)
      .map(user => user.name)
    

    TypeScript Essentials

    All TypeScript Types

    | Type | Description |

    |------|-------------|

    | number | Numeric values |

    | bigint | Large integers |

    | boolean | true or false |

    | string | Text values |

    | array | T[] or Array |

    | tuple | Fixed-length arrays with specific types |

    | enum | Named constants |

    | unknown | Type-safe any |

    | any | Opt out of type checking |

    | void | No return value |

    | null | Intentional absence |

    | undefined | Uninitialized |

    | never | Never returns (throws/infinite loop) |

    | object | Non-primitive type |


    Variable Declarations

    | Keyword | Scope | Reassignable | Hoisted |

    |---------|-------|--------------|---------|

    | var | Function | Yes | Yes (initialized as undefined) |

    | let | Block | Yes | No (temporal dead zone) |

    | const | Block | No | No (temporal dead zone) |

    > Note: const prevents reassignment, not mutation. Object properties can still be changed unless marked readonly.


    Destructuring

    Array Destructuring

    
    const input = [1, 2, 3]
    const [first, second] = input
    
    // Skip elements
    const [first, , third] = [10, 20, 30]
    
    // Swap variables
    [first, second] = [second, first]
    

    Object Destructuring

    
    const user = { name: "Ian", age: 30 }
    const { name } = user // "Ian"
    

    Default Values

    
    function greet({ name, greeting = "Hello" }: { name: string; greeting?: string }) {
      return `${greeting}, ${name}!`
    }
    

    Spread Operator

    
    const arr = [1, 2, 3]
    const newArr = [...arr, 4] // [1, 2, 3, 4]
    
    const obj = { a: 1 }
    const newObj = { ...obj, b: 2 } // { a: 1, b: 2 }
    

    Interfaces

    Define object shapes:

    
    interface User {
      name: string
      id: number
    }
    
    const user: User = {
      name: "Hayes",
      id: 0
    }
    

    Composing Types

    Union Types

    
    type Status = "loading" | "success" | "error"
    type StringOrNumber = string | number
    

    Generics

    
    type StringArray = Array<string>
    type NumberArray = Array<number>
    
    // Generic function
    function identity<T>(value: T): T {
      return value
    }
    

    TypeScript Utility Types

    Omit

    Creates a new type by excluding specific properties.

    
    type User = {
      id: number
      name: string
      email: string
    }
    
    type PublicUser = Omit<User, "email">
    // Result: { id: number; name: string }
    

    Record

    Creates a dictionary/map type with keys of type K and values of type V.

    
    type Scores = Record<string, number>
    
    const scores: Scores = {
      alice: 10,
      bob: 15
    }
    

    Equivalent to:

    
    type Scores = {
      [key: string]: number
    }
    

    Pick

    Creates a new type by selecting specific properties.

    
    type User = {
      id: number
      name: string
      email: string
    }
    
    type UserPreview = Pick<User, "id" | "name">
    // Result: { id: number; name: string }
    

    Partial

    Makes all properties optional.

    
    type User = {
      id: number
      name: string
    }
    
    type PartialUser = Partial<User>
    // Result: { id?: number; name?: string }
    

    Use case: Useful for update functions where you only want to modify some fields.


    Required

    Makes all properties required (opposite of Partial).

    
    type User = {
      id?: number
      name?: string
    }
    
    type RequiredUser = Required<User>
    // Result: { id: number; name: string }
    

    Readonly

    Makes all properties readonly.

    
    type User = {
      id: number
      name: string
    }
    
    type ReadonlyUser = Readonly<User>
    // Cannot modify properties after creation
    

    ReturnType

    Extracts the return type of a function.

    
    function getUser() {
      return { id: 1, name: "Ian" }
    }
    
    type User = ReturnType<typeof getUser>
    // Result: { id: number; name: string }
    

    Parameters

    Extracts function parameter types as a tuple.

    
    function greet(name: string, age: number) {}
    
    type GreetParams = Parameters<typeof greet>
    // Result: [string, number]
    

    Type Guards

    Narrow types at runtime using type predicates:

    
    function isString(value: unknown): value is string {
      return typeof value === "string"
    }
    
    function process(value: string | number) {
      if (isString(value)) {
        console.log(value.toUpperCase()) // TS knows it's string
      }
    }
    

    Common Type Guards

    
    // typeof
    if (typeof x === "string") { }
    
    // instanceof
    if (error instanceof Error) { }
    
    // in operator
    if ("name" in obj) { }
    
    // Array.isArray
    if (Array.isArray(items)) { }
    

    as const Assertions

    Makes values deeply readonly and infers literal types:

    
    const colors = ["red", "green", "blue"] as const
    // Type: readonly ["red", "green", "blue"]
    // Without: string[]
    
    const config = {
      endpoint: "/api",
      timeout: 3000
    } as const
    // All properties are readonly and literal typed
    

    Use case: Creating union types from arrays:

    
    const statuses = ["pending", "active", "done"] as const
    type Status = typeof statuses[number] // "pending" | "active" | "done"
    

    Type vs Interface

    | Feature | type | interface |

    |---------|--------|-------------|

    | Union types | βœ… | ❌ |

    | Declaration merging | ❌ | βœ… |

    | Extends | βœ… (via &) | βœ… (via extends) |

    | Computed properties | βœ… | ❌ |

    Interface Extension

    
    interface Person {
      name: string
    }
    
    interface Employee extends Person {
      salary: number
    }
    

    Type Intersection

    
    type Point = { x: number; y: number }
    type NamedPoint = Point & { name: string }
    

    Declaration Merging (Interface only)

    
    interface User {
      name: string
    }
    
    interface User {
      age: number
    }
    
    // Result: User has both name and age
    

    Node.js & Package Management

    What is Node.js?

    Node.js is a JavaScript runtime built on Chrome's V8 engine. It allows you to run JavaScript outside the browser (on servers, CLI tools, etc.).

    | Feature | Browser JS | Node.js |

    |---------|-----------|---------||

    | DOM access | βœ… | ❌ |

    | window object | βœ… | ❌ |

    | document object | βœ… | ❌ |

    | File system access | ❌ | βœ… |

    | process object | ❌ | βœ… |

    | require/import modules | βœ… (ESM) | βœ… (both) |

    Key Point: Node.js is single-threaded but uses an event loop for async I/O (same concept as browser JS).


    npm vs npx vs yarn vs pnpm

    | Tool | Purpose |

    |------|---------||

    | npm | Node Package Manager β€” installs, manages, and publishes packages |

    | npx | Executes packages without installing globally (e.g., npx create-react-app) |

    | yarn | Alternative to npm (faster, deterministic installs, by Facebook) |

    | pnpm | Performant npm β€” uses symlinks, saves disk space |

    Interview Tip: Know that npx is useful for running one-off commands without polluting global installs.

    
    # npm - install then run
    npm install -g create-react-app
    create-react-app my-app
    
    # npx - run directly without global install
    npx create-react-app my-app
    

    package.json

    The manifest file for your project. Contains metadata, dependencies, and scripts.

    
    {
      "name": "my-app",
      "version": "1.0.0",
      "main": "index.js",
      "type": "module",
      "scripts": {
        "start": "node index.js",
        "dev": "nodemon index.js",
        "build": "tsc",
        "test": "jest"
      },
      "dependencies": {
        "express": "^4.18.0"
      },
      "devDependencies": {
        "typescript": "^5.0.0",
        "jest": "^29.0.0"
      }
    }
    

    | Field | Description |

    |-------|-------------|

    | name | Package name (must be unique if publishing) |

    | version | Current version (semver) |

    | main | Entry point for CommonJS |

    | module | Entry point for ES Modules |

    | type | "module" for ESM, "commonjs" (default) for CJS |

    | scripts | Custom commands run via npm run |

    | dependencies | Production packages |

    | devDependencies | Development-only packages |


    Dependencies vs DevDependencies

    | Type | Install Command | Purpose | Included in Production? |

    |------|-----------------|---------|------------------------|

    | dependencies | npm install lodash | Required at runtime | βœ… Yes |

    | devDependencies | npm install -D jest | Development/build tools | ❌ No |

    Examples:

    | dependencies | devDependencies |

    |--------------|------------------|

    | react, express, axios | typescript, jest, eslint |

    | lodash, moment | webpack, vite, prettier |

    Interview Question: "Why separate them?"

  • Smaller production bundles
  • Faster installs in CI/CD (npm install --production)
  • Clear distinction of what's needed at runtime

  • Semantic Versioning (SemVer)

    Format: MAJOR.MINOR.PATCH (e.g., 4.18.2)

    | Part | When to Increment | Example |

    |------|-------------------|----------|

    | MAJOR | Breaking changes | 4.0.0 β†’ 5.0.0 |

    | MINOR | New features (backward compatible) | 4.18.0 β†’ 4.19.0 |

    | PATCH | Bug fixes (backward compatible) | 4.18.2 β†’ 4.18.3 |

    Version Ranges in package.json

    | Symbol | Meaning | Example | Matches |

    |--------|---------|---------|----------|

    | ^ (caret) | Compatible with version | ^4.18.0 | 4.18.0 to <5.0.0 |

    | ~ (tilde) | Approximately equivalent | ~4.18.0 | 4.18.0 to <4.19.0 |

    | | Any version | | Latest |

    | >=, < | Range | >=4.0.0 <5.0.0 | Explicit range |

    | (none) | Exact version | 4.18.2 | Only 4.18.2 |

    Interview Tip: ^ is the default when you npm install. It allows minor and patch updates.


    package-lock.json

    Purpose: Locks the exact versions of all dependencies (including nested ones).

    | package.json | package-lock.json |

    |--------------|-------------------|

    | "express": "^4.18.0" | "express": "4.18.2" (exact) |

    | Version ranges | Exact resolved versions |

    | Human-editable | Auto-generated |

    | Commit? Yes | Commit? Yes |

    Why commit it?

  • Ensures everyone gets the same versions
  • Reproducible builds across machines/CI
  • Prevents "works on my machine" issues
  • Interview Question: "What happens if you delete package-lock.json?"

  • npm will resolve versions again based on package.json ranges
  • You might get different (newer) versions
  • Could introduce bugs or breaking changes

  • node_modules

    The folder where all installed packages live.

    Key Points:

  • Never commit to git (add to .gitignore)
  • Can be huge (hundreds of MB)
  • Recreated with npm install
  • Contains all dependencies AND their dependencies (nested)
  • 
    # Typical .gitignore
    node_modules/
    .env
    dist/
    

    Interview Question: "Why not commit node_modules?"

  • Too large
  • Platform-specific binaries
  • package-lock.json already guarantees reproducibility

  • Common npm Commands

    | Command | Description |

    |---------|-------------|

    | npm init | Create package.json interactively |

    | npm init -y | Create package.json with defaults |

    | npm install | Install all dependencies from package.json |

    | npm install | Install and add to dependencies |

    | npm install -D | Install and add to devDependencies |

    | npm install -g | Install globally |

    | npm uninstall | Remove a package |

    | npm update | Update packages to latest allowed version |

    | npm outdated | Check for outdated packages |

    | npm run