13 questions on JavaScript and TypeScript: closures, the event loop, promises, this binding, equality, optional chaining, type versus interface, unions and narrowing, and generics.
01JS / TSFoundation
let, const, var
Answer
var is function-scoped and hoisted. Redeclaration in the same function is allowed. Temporal dead zone does not apply the same way as let.
let and const are block-scoped. They are hoisted but stay in the temporal dead zone until initialization.
const cannot be reassigned. The binding is fixed. Object and array contents can still mutate unless you freeze them.
Prefer const by default. Use let when reassignment is required. Avoid var in modern application code.
Loop bindings: let creates a new binding per iteration, which fixes classic closure-in-loop bugs that var shows.
Pitfall: const user = {}; user.name = 'Ada' is allowed. const only protects the binding, not deep immutability.
Shadowing inner block lets can hide outer bindings and confuse readers. Prefer clear names over reuse.
In modules and strict mode, accidental globals from missing declarations throw. Always declare.
Interview tip: contrast function scope versus block scope with a short for-loop example.
A closure is the combination of a function bundled together with references to its surrounding lexical environment. A closure gives an inner function access to an outer function's scope even after the outer function has finished executing and returned.
Lexical Scoping: JavaScript resolves variable names based on the physical nesting of functions in source code. When a function executes, it holds a hidden reference to its outer Environment Record, forming a Scope Chain.
Memory Retention: Variables captured by a closure are not garbage-collected when the parent function exits. They remain allocated on the heap for as long as any active function retains a reference to that closure.
Primary Use Cases: Data encapsulation and private state (Module Pattern, factory functions), function currying and partial application, and event callback handling.
Flow diagram: Closure scope chain retaining outer variables on the heap
JavaScript (Private Encapsulation via Closure)
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private state, inaccessible from outsidereturn {
deposit(amount) {
if (amount <= 0) thrownewError("Deposit must be positive");
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) thrownewError("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
console.log(account.getBalance()); // 150
console.log(account.balance); // undefined (Private state safely protected)
03JS / TSFoundation
JavaScript Event Loop: Call Stack, Microtasks, and Macrotasks
Answer
JavaScript has a single-threaded runtime model based on an Event Loop. The execution engine runs code sequentially on a single Call Stack and delegates asynchronous tasks (such as network calls and timers) to the browser or host environment.
Call Stack: Synchronous code executes immediately by pushing and popping execution frames.
Microtask Queue: Contains Promise callbacks (.then, .catch, .finally), queueMicrotask, and MutationObserver callbacks. Microtasks have higher priority than macrotasks; once the Call Stack empties, the engine exhausts the entire Microtask Queue before moving forward.
Macrotask Queue (Task Queue): Contains callbacks from setTimeout, setInterval, setImmediate, and I/O events. The Event Loop dequeues one macrotask per tick, exhausts all microtasks spawned by it, allows the browser to re-render the DOM if needed, and moves to the next tick.
Flow diagram: Event Loop priority: Call Stack to Microtasks to Render to Macrotasks
A Promise represents a future result with states pending, fulfilled, or rejected. Settlement is one-way.
async functions always return Promises. await pauses the async function until the awaited promise settles, then continues with the value or throws on rejection.
Always handle rejection with try/catch around await, or with .catch on the promise chain.
Promise.all fails fast on the first rejection. Promise.allSettled waits for every input to settle.
Promise.race settles with the first success or failure. Promise.any fulfills with the first fulfillment and rejects only if all reject.
Map async work carefully. forEach does not await. Use for...of with await or Promise.all on a mapped array.
Pitfall: floating promises without catch become unhandled rejections.
Prefer async/await for linear control flow. Use combinators when you need concurrency patterns.
Do not wrap an existing promise in new Promise unless you are adapting a callback API.
State diagram: Promise states
TS
asyncfunction loadUser(id: string) {
const res = await fetch("/api/users/" + id);
if (!res.ok) thrownewError("HTTP " + res.status);
return res.json();
}
09JS / TSIntermediate
Union types and narrowing
Answer
Union types allow a value to be one of several shapes. Narrow before you access members that are not shared.
Narrow with typeof, instanceof, in, equality checks, and user-defined type predicates (value is Type).
Discriminated unions share a tag field (for example kind: 'ok' | 'err'). Switch on the tag for safe field access.
unknown is safer than any. You must narrow unknown before calling or reading properties.
Control flow analysis follows returns, throws, and early continues. After if (!x) return, x is narrowed in the rest of the block.
Pitfall: using as Type assertions to silence errors without proving the value matches.
never appears when a switch is exhaustive. Use it to catch missing union members at compile time.
Optional properties and nullability interact with strictNullChecks. Enable that flag for real narrowing benefits.
Interview tip: write a small Result union and show a switch that TypeScript accepts as exhaustive.