Learning Plan: JavaScript & TypeScript Mid-Advanced
This plan covers the essential concepts to go from “I can work with them” to “I understand how they work”. Each section includes what you need to know and common interview questions with answers.
📦 MODULE 1: Advanced JavaScript – Under the Hood
1.1 Event Loop, Call Stack, Task Queue
What you need to understand:
- Call Stack – synchronous execution, LIFO
- Web APIs – timers, fetch, DOM events (run outside the JS engine)
- Callback Queue (macrotasks) – setTimeout, setInterval, I/O
- Microtask Queue – Promise.then, queueMicrotask, MutationObserver
- Priority order: Call Stack → Microtasks → Macrotasks
Mental model example:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2Interview questions:
Q: What is the event loop and how does it work?
The event loop is the mechanism that allows JavaScript to be non-blocking despite being single-threaded. It constantly checks if the call stack is empty, then moves tasks from the queue into the stack. The microtask queue (Promise.then) has priority over the macrotask queue (setTimeout).
Q: What’s the difference between microtasks and macrotasks?
Microtasks (Promise callbacks, MutationObserver) are processed immediately after the call stack empties, before any macrotask (setTimeout, setInterval, I/O events). This means Promise.resolve().then() executes before a setTimeout with delay 0.
1.2 Closures
What you need to understand:
- A function “closes over” the variables from the scope in which it was created
- Closure = function + reference to its lexical scope
- Use cases: encapsulation, module pattern, memoization, partial application
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count
};
}
const counter = makeCounter();
counter.increment(); // 1
counter.get(); // 1Interview questions:
Q: What is a closure?
A closure is a function that retains access to the variables in the lexical scope in which it was defined, even after that scope is no longer active. A closure is formed every time a function is created.
Q: The classic closure-in-loop problem:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 3, 3, 3 — not 0, 1, 2
// Fix with let (block scope) or an IIFEWith
var, all callbacks reference the same variablei. Withlet, each iteration creates a new binding.
1.3 Prototypal Inheritance & this
What you need to understand:
- Every object has an internal
[[Prototype]](accessible via__proto__orObject.getPrototypeOf) classin JS is syntax sugar over the prototype chainthisis determined at call time, not at define time (exception: arrow functions)
Rules for this:
- Default binding – regular function:
this= global /undefinedin strict mode - Implicit binding – method on an object:
this= the object to the left of the dot - Explicit binding –
.call(),.apply(),.bind() - New binding –
new Constructor():this= the newly created object - Arrow functions – have no own
this, they inherit it from the lexical scope
Interview questions:
Q: What value does this have in different contexts?
const obj = {
name: "test",
regular: function() { return this.name; }, // "test"
arrow: () => this.name // undefined (or global.name)
};Q: How does the prototype chain work?
When you access a property on an object, JS looks for it first on the object itself, then on its
[[Prototype]], then on the prototype’s prototype, and so on, up toObject.prototype. If it’s not found anywhere, it returnsundefined.
1.4 Scope, Hoisting, Temporal Dead Zone
What you need to understand:
var– function scoped, hoisted with valueundefinedlet/const– block scoped, hoisted but in TDZ (cannot be accessed before declaration)- Function declarations – fully hoisted (including the definition)
- Function expressions – only the variable is hoisted, not the value
console.log(x); // undefined (var hoisted)
console.log(y); // ReferenceError (TDZ)
var x = 1;
let y = 2;Interview questions:
Q: What is the Temporal Dead Zone?
The TDZ is the period between the start of a block scope and the point of the
let/constdeclaration. Accessing the variable during this period throws aReferenceError, even though technically the variable has been hoisted.
1.5 Asynchronous JavaScript
What you need to understand:
- Callbacks → Callback Hell
- Promises: states (pending, fulfilled, rejected), chaining
Promise.all,Promise.allSettled,Promise.race,Promise.anyasync/await– syntax sugar over Promises- Error handling:
.catch()vstry/catch
// Promise.all – fails if any one fails
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
// Promise.allSettled – waits for all, regardless of result
const results = await Promise.allSettled([fetchA(), fetchB()]);Interview questions:
Q: What’s the difference between Promise.all and Promise.allSettled?
Promise.allrejects immediately if any promise fails (fail-fast).Promise.allSettledwaits for all to finish and returns an array with the status of each one ({status: "fulfilled", value}or{status: "rejected", reason}).
Q: What happens if await is used without try/catch?
If the promise is rejected, the error propagates and can result in an unhandled rejection. Best practice: always wrap with
try/catchor add.catch().
🧩 MODULE 2: Advanced JavaScript – Patterns & Techniques
2.1 Immutability & Pure Functions
What you need to understand:
- Reference vs value (primitives vs objects)
- Spread operator for shallow copy
structuredClone()for deep copy- Pure functions: same input → same output, no side effects
// Shallow copy – the problem
const a = { x: { y: 1 } };
const b = { ...a };
b.x.y = 99; // also modifies a.x.y!
// Deep copy
const c = structuredClone(a); // ES2022Interview questions:
Q: What’s the difference between a shallow and a deep copy?
A shallow copy creates a new object, but properties that are references (nested objects, arrays) are copied as references, not as new values. A deep copy creates recursive copies for all levels.
2.2 Higher-Order Functions & Functional Programming
What you need to understand:
map,filter,reduce– internal implementation- Currying and partial application
- Function composition
flatMap
// Currying
const multiply = (a) => (b) => a * b;
const double = multiply(2);
double(5); // 10
// Compose
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);Interview questions:
Q: How do you implement reduce manually?
Array.prototype.myReduce = function(callback, initialValue) {
let acc = initialValue !== undefined ? initialValue : this[0];
let start = initialValue !== undefined ? 0 : 1;
for (let i = start; i < this.length; i++) {
acc = callback(acc, this[i], i, this);
}
return acc;
};2.3 Design Patterns in JS
What you need to know:
- Module Pattern – encapsulation with IIFE / ES Modules
- Observer/EventEmitter – publish-subscribe
- Factory Pattern – create objects without
new - Singleton – a single instance
- Proxy – intercept operations on objects
// Proxy – useful for validation, logging, reactive systems
const handler = {
set(target, prop, value) {
if (typeof value !== "number") throw new TypeError("Only numbers!");
target[prop] = value;
return true;
}
};
const obj = new Proxy({}, handler);Interview questions:
Q: How does Proxy work and where is it used in practice?
Proxyallows you to intercept and customize fundamental operations on an object (get, set, delete, etc.). It’s used in reactive systems (Vue 3 uses it for reactivity), data validation, logging, and automatic memoization.
🔷 MODULE 3: Advanced TypeScript
3.1 The Type System in Depth
What you need to understand:
- Structural typing (duck typing) – not nominal typing
- Type widening and narrowing
unknownvsanyvsnever- Discriminated unions
// unknown is safer than any
function process(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // OK, narrowed
}
}
// never – impossible type, useful for exhaustive checks
type Shape = "circle" | "square";
function getArea(shape: Shape): number {
switch (shape) {
case "circle": return Math.PI;
case "square": return 1;
default:
const _exhaustive: never = shape; // error if Shape has unhandled values
throw new Error(`Unknown: ${_exhaustive}`);
}
}Interview questions:
Q: What’s the difference between unknown and any?
anycompletely disables type checking – you can do anything with ananyvalue.unknownis type-safe: you must narrow the type before using the value. Preferunknownfor external inputs (API responses, user input).
Q: What is structural typing?
TypeScript checks type compatibility by structure (what properties it has), not by name. If two types have the same structure, they are compatible, even if they have different names.
3.2 Generics
What you need to understand:
- Generic functions, classes, interfaces
- Generic constraints with
extends - Default type parameters
- Type inference with generics
// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Generic with default
interface ApiResponse<T = unknown> {
data: T;
status: number;
}Interview questions:
Q: When do you use generics?
Generics are useful when you want to write reusable code that works with multiple types while maintaining type safety. E.g.: utility functions (
identity,map,filter), React components (useState<T>()), API call wrappers.
3.3 Utility Types
What you need to know:
| Utility Type | What it does |
|---|---|
Partial<T> | All properties become optional |
Required<T> | All properties become required |
Readonly<T> | No property can be mutated |
Pick<T, K> | Select only certain properties |
Omit<T, K> | Exclude certain properties |
Record<K, V> | Object with keys of type K and values V |
Exclude<T, U> | Exclude types from a union |
Extract<T, U> | Keep types from a union |
ReturnType<T> | The return type of a function |
Parameters<T> | The parameter types of a function |
type User = { id: number; name: string; email: string };
type UpdateUser = Partial<Pick<User, "name" | "email">>;
// { name?: string; email?: string }Interview questions:
Q: How do you implement Partial<T> manually?
type MyPartial<T> = {
[K in keyof T]?: T[K];
};3.4 Conditional Types & Mapped Types
What you need to understand:
T extends U ? X : Y- The
inferkeyword - Mapped types with modifiers (
+?,-?,readonly) - Template literal types
// Conditional type with infer
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnpackPromise<Promise<string>>; // string
// Template literal types
type EventNames = "click" | "focus" | "blur";
type Handlers = `on${Capitalize<EventNames>}`;
// "onClick" | "onFocus" | "onBlur"Interview questions:
Q: What does infer do in TypeScript?
inferlets you “extract” a type from a more complex type within a conditional type. It’s frequently used to extract the resolved value type from a Promise, the element type from an array, or the argument types from a function.
3.5 Declaration Merging & Module Augmentation
What you need to understand:
- Interface merging (interfaces with the same name are merged)
- Augmenting external modules
declare module,declare global
// Augmentation – adding types to an external module
declare module "express" {
interface Request {
user?: { id: string; role: string };
}
}3.6 TypeScript Compiler & tsconfig
Important options to know:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"moduleResolution": "bundler",
"paths": {}
}
}Interview questions:
Q: What does strict: true enable?
It enables a set of checks:
strictNullChecks(null/undefined are not assignable to other types),strictFunctionTypes,strictBindCallApply,noImplicitAny,noImplicitThis, and others.
🎯 MODULE 4: Interview Questions – Mixed JS/TS
Q: Explain the difference between == and ===.
===(strict equality) compares both type and value, with no coercion.==(loose equality) performs type coercion before comparing (e.g."5" == 5istrue). Always use===.
Q: What is memoization and how do you implement it?
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}Q: How does WeakMap work and when do you use it?
WeakMapholds “weak” references to keys (objects only). If the key object has no other references, it gets garbage collected along with the WeakMap entry. Useful for caching without memory leaks and for storing private data associated with an object.
Q: What is debounce and throttle?
Debounce – delays execution until a certain time has passed since the last call (use case: search input). Throttle – limits execution to at most once per time interval (use case: scroll events, resize).
Q: Explain the difference between interface and type in TypeScript.
Both can describe the shape of an object. Key differences:
interfacesupports declaration merging and is preferred for public APIs;typecan express unions, intersections, mapped types, and conditional types. In practice, for objects both work, buttypeis more flexible for complex types.
Q: What is the satisfies operator in TypeScript?
const config = {
port: 3000,
host: "localhost"
} satisfies Record<string, string | number>;
// The type remains the inferred (specific) one, but is validated against Record
satisfiesvalidates that a value conforms to a type, without losing the more specific inferred type. Different fromas(which forces a type) and: Type(which widens the type).
📚 Recommended Resources
- overreacted.io – Dan Abramov, deep dives into JS/React
- you-dont-know-js – free on GitHub, the best resource for deep JS
- typescript-deep-dive – Basarat Ali Syed
- total-typescript.com – Matt Pocock, the best for advanced TS
- javascript.info – complete, modern reference
Plan designed for mid-advanced level. Estimated completion time: 4–8 weeks, depending on pace.