1. Quick Comparison Tables (Interview Essentials)
Observables vs Promises (Top Interview Question)
| Feature | RxJS Observables | JavaScript Promises |
|---|---|---|
| Value Emissions | Emits multiple values over time (0, 1, or infinite stream) | Emits only a single value (or error) then completes |
| Execution | Lazy (Does nothing until .subscribe() is called) |
Eager (Executes code immediately upon creation) |
| Cancellability | Cancellable (unsubscribe(), switchMap, takeUntil) |
Non-cancellable natively once initiated |
| Operators Support | Powerful ecosystem of 100+ operators (map, switchMap, debounceTime) |
Basic chaining via .then(), .catch(), .finally() |
| Data Flow Type | Push-based stream (Producer pushes data over time) | One-time async resolution |
| Multicasting | Cold by default (unicast), can be made Hot (shareReplay) |
Hot by default (multicast — same value shared) |
| Angular Integration | Built-in framework core (HttpClient, Reactive Forms, Router) |
Converted via firstValueFrom() or lastValueFrom() |
Higher-Order Mapping Operators
| Operator | Behavior | Metaphor | Best Used For |
|---|---|---|---|
switchMap |
Cancels previous request, switches to latest | Elevator door timer resetting when new person arrives | Live Search / Typeahead Input |
mergeMap |
Runs all requests concurrently in parallel | Multiple supermarket checkout registers open at once | Bulk File Uploads / Parallel Fetches |
concatMap |
Queues requests, runs strictly 1-by-1 in order | Single file ticket queue line | Bank Transactions / Sequential Saves |
exhaustMap |
Ignores incoming triggers until current finishes | Locked door while bathroom is occupied | Form Submit / Login Button Click |
Subject Types Comparison
| Subject Type | Initial Value? | Replays Past Values? | Best Used For |
|---|---|---|---|
Subject |
No | No (Late subscribers miss past emissions) | Event emitters, button clicks, toast notifications |
BehaviorSubject |
Yes (Required) | Yes (Replays current/latest value immediately) | Shared app state (Current User, Theme) |
ReplaySubject(N) |
No | Yes (Replays last N values) | Audit logs, recent action history |
AsyncSubject |
No | Yes (Only emission upon stream completion) | Operations emitting once when finished |
2. Event Loop Execution Order: setTimeout vs Promise vs Observable.subscribe()
🔥 Top Interview Snippet
console.log('1: Sync Start');
setTimeout(() => {
console.log('2: setTimeout (Macrotask)');
}, 0);
Promise.resolve().then(() => {
console.log('3: Promise (Microtask)');
});
new Observable((subscriber) => {
console.log('4: Observable Executor (Sync)');
subscriber.next('5: Observable Value (Sync)');
}).subscribe((val) => {
console.log(val);
});
console.log('6: Sync End');
Console Output Produced:
1: Sync Start
4: Observable Executor (Sync)
5: Observable Value (Sync)
6: Sync End
3: Promise (Microtask)
2: setTimeout (Macrotask)
Step-by-Step Execution Sequence Explained:
| Order | Output Log | Queue Type | Why it executes at this exact moment |
|---|---|---|---|
| 1st | 1: Sync Start |
Call Stack (Sync) | Main synchronous script execution starts. |
| 2nd | 4: Observable Executor (Sync) |
Call Stack (Sync) | Crucial Interview Point: Subscribing to a standard Observable runs synchronously! |
| 3rd | 5: Observable Value (Sync) |
Call Stack (Sync) | subscriber.next() fires the observer callback synchronously inside .subscribe(). |
| 4th | 6: Sync End |
Call Stack (Sync) | Main synchronous script finishes execution. |
| 5th | 3: Promise (Microtask) |
Microtask Queue | Promise .then() callback is placed in Microtask Queue. Microtasks execute immediately after Call Stack empties, before Macrotasks! |
| 6th | 2: setTimeout (Macrotask) |
Macrotask Queue | setTimeout(..., 0) callback sits in Macrotask (Task) Queue. Executes only after all Microtasks are cleared. |
🧠 The Event Loop Golden Rules for Technical Interviews:
- Rule 1: Call Stack (Synchronous Code) Always Executes First
The JS engine executes synchronous code line-by-line until Call Stack is completely empty. Gotcha: Subscribing to a standard RxJS Observable is synchronous! - Rule 2: Microtask Queue Executes Second (Flushes Completely)
Once Call Stack is empty, Event Loop clears the entire Microtask Queue before touching any Macrotask! (IncludesPromise.then(),queueMicrotask(),MutationObserver). - Rule 3: Macrotask Queue Executes Third (One per Tick)
Event Loop picks one single macrotask, executes it, then checks Microtask Queue again! (IncludessetTimeout,setInterval, DOM events).
3. Higher-Order Mapping & Transformation
3.1 switchMap (Cancel Previous, Switch to Latest)
Real-Life Scenario: Live Search / Typeahead Autocomplete
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.http.get(`/api/search?q=${query}`))
).subscribe(results => this.searchResults = results);
3.2 mergeMap / flatMap (Concurrent Execution)
Real-Life Scenario: Multi-file Upload Manager
from(selectedFiles).pipe(
mergeMap(file => this.uploadService.uploadFile(file))
).subscribe(res => console.log('Uploaded:', res));
3.3 concatMap (Sequential Order Guaranteed)
Real-Life Scenario: Financial Payments / Step-by-Step Saves
from(pendingTransactions).pipe(
concatMap(tx => this.paymentService.processPayment(tx))
).subscribe(result => console.log('Processed in order:', result));
3.4 exhaustMap (Ignore New Until Current Finishes)
Real-Life Scenario: Login / Form Submit Button (Prevent Spam)
this.loginClick$.pipe(
exhaustMap(() => this.authService.login(this.form.value))
).subscribe(user => this.router.navigate(['/dashboard']));
3.5 map (Transform Values)
Real-Life Scenario: Data Transformation / DTO Mapping
this.http.get<UserDto>('/api/user/1').pipe(
map(user => ({
fullName: `${user.firstName} ${user.lastName}`,
isAdult: user.age >= 18
}))
).subscribe(userProfile => console.log(userProfile));
3.6 scan (Accumulate Values Over Time)
Real-Life Scenario: Shopping Cart Total / Running Accumulator
this.addToCart$.pipe(
scan((totalCount, item) => totalCount + item.quantity, 0)
).subscribe(totalItems => this.cartBadge = totalItems);
4. Combination Operators
4.1 forkJoin (Promise.all equivalent)
Real-Life Scenario: Dashboard Page Load
forkJoin({
profile: this.userService.getProfile(),
roles: this.userService.getRoles(),
settings: this.settingsService.getSettings()
}).subscribe(({ profile, roles, settings }) => {
this.initDashboard(profile, roles, settings);
});
4.2 combineLatest (Re-evaluate when ANY source changes)
Real-Life Scenario: Multi-Filter Table (Search + Category + Date)
combineLatest([
this.searchQuery$,
this.selectedCategory$,
this.selectedDate$
]).pipe(
switchMap(([query, category, date]) =>
this.productService.filterProducts(query, category, date)
)
).subscribe(filteredProducts => this.products = filteredProducts);
4.3 withLatestFrom (Primary Trigger + Secondary Snapshot)
Real-Life Scenario: Save Action with User Profile Snapshot
this.saveButtonClick$.pipe(
withLatestFrom(this.userProfile$),
switchMap(([_, user]) => this.documentService.save(this.docData, user.id))
).subscribe();
4.4 merge (Combine Multiple Event Streams)
Real-Life Scenario: Refresh Trigger (Manual Click + 30s Auto Timer)
merge(
this.refreshButtonClick$,
timer(0, 30000) // auto refresh every 30 seconds
).pipe(
switchMap(() => this.dataService.fetchLatestData())
).subscribe(data => this.updateGrid(data));
4.5 startWith (Provide Initial Value)
Real-Life Scenario: Initial Loading Spinner / Default Value Emitting
this.searchQuery$.pipe(
startWith(''), // Start with empty search on page load
switchMap(query => this.fetchItems(query))
).subscribe(items => this.items = items);
5. Rate Limiting & Filtering Operators
5.1 debounceTime (Pause / Quiet Period)
Real-Life Scenario: Search Input Keypress Pause
this.searchInput.valueChanges.pipe(
debounceTime(300)
).subscribe(text => this.search(text));
5.2 throttleTime (First Event Immediately, Silence for N ms)
Real-Life Scenario: Window Resize / Scroll Event / Click Spam Prevention
this.scrollEvent$.pipe(
throttleTime(1000)
).subscribe(() => this.loadMoreItems());
5.3 distinctUntilChanged (Ignore Duplicate Emitted Values)
Real-Life Scenario: Avoiding Redundant API Calls
this.searchTerm$.pipe(
distinctUntilChanged()
).subscribe(query => this.fetchResults(query));
5.4 takeUntilDestroyed / takeUntil (Memory Leak Cleanup)
Real-Life Scenario: Unsubscribing when Component Unmounts
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.dataStream$.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(data => this.processData(data));
}
6. Multicasting, Caching & Error Handling
6.1 shareReplay (Cache & Multicast HTTP Responses)
Real-Life Scenario: Caching Lookup Data (Categories, Brands, Employees)
getCompanyList(): Observable<Company[]> {
if (!this.companyList$) {
this.companyList$ = this.http.get<Company[]>('/api/companies').pipe(
shareReplay(1) // Cache response & share stream
);
}
return this.companyList$;
}
6.2 catchError (Graceful Error Handling)
Real-Life Scenario: Intercepting HTTP Failures gracefully
this.http.get('/api/products').pipe(
catchError(error => {
this.notificationService.showError('Failed to load products');
return of([]); // Safe fallback array
})
).subscribe(products => this.products = products);
6.3 retry / retryWhen (Automatic Network Retry)
Real-Life Scenario: Intermittent Network Glitches / Flaky Connections
this.http.get('/api/unstable-endpoint').pipe(
retry(3), // Retry up to 3 times on failure
catchError(err => of(null))
).subscribe();
6.4 finalize (Execute Cleanup Always)
Real-Life Scenario: Hiding Loading Spinner / Resetting Buttons
this.isLoading = true;
this.http.get('/api/data').pipe(
finalize(() => this.isLoading = false) // Always turns off spinner
).subscribe();
6.5 tap (Side Effects & Debugging)
Real-Life Scenario: Logging & State Side-effects without data mutation
this.http.get('/api/offers').pipe(
tap(() => this.showLoader = true),
tap(data => console.log('Data:', data))
).subscribe();
7. Advanced JavaScript Array Concepts (6-7+ YOE Interview Questions)
7.1 Custom Array Polyfills (Writing myMap & myReduce from Scratch)
Real-Life Scenario: Demonstrating deep understanding of JavaScript prototypes, callback execution, and this context binding.
// 1. Custom Array.prototype.myMap
Array.prototype.myMap = function (callback, thisArg) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (i in this) { // Handles sparse arrays correctly
result.push(callback.call(thisArg, this[i], i, this));
}
}
return result;
};
// 2. Custom Array.prototype.myReduce
Array.prototype.myReduce = function (callback, initialValue) {
let accumulator = initialValue !== undefined ? initialValue : this[0];
let startIndex = initialValue !== undefined ? 0 : 1;
for (let i = startIndex; i < this.length; i++) {
if (i in this) {
accumulator = callback(accumulator, this[i], i, this);
}
}
return accumulator;
};
7.2 Grouping Flat Array Data into Lookup Objects (reduce & Object.groupBy)
Real-Life Scenario: Grouping an API response array of orders by status (e.g. { 'PENDING': [...], 'SHIPPED': [...] }).
const orders = [
{ id: 1, status: 'PENDING', amount: 100 },
{ id: 2, status: 'SHIPPED', amount: 200 },
{ id: 3, status: 'PENDING', amount: 150 }
];
// Traditional reduce grouping (Compatible across all browsers)
const groupedByStatus = orders.reduce((acc, order) => {
acc[order.status] = acc[order.status] || [];
acc[order.status].push(order);
return acc;
}, {});
// Modern ES2024 Object.groupBy
const modernGrouped = Object.groupBy(orders, order => order.status);
7.3 Immutable Array Operations (toSorted, toSpliced, toReversed, with)
Real-Life Scenario: Updating component state in Angular Signals or Redux without mutating original array references.
const originalList = [3, 1, 4, 2];
const sortedList = originalList.toSorted(); // [1, 2, 3, 4]
const reversedList = originalList.toReversed(); // [2, 4, 1, 3]
const updatedItem = originalList.with(0, 99); // [99, 1, 4, 2]
console.log(originalList); // Still [3, 1, 4, 2] (Pure & Unchanged!)
7.4 flat() & flatMap()
Real-Life Scenario: Extracting nested child arrays from API items in 1 line.
const customers = [
{ name: 'Alice', tags: ['VIP', 'Tech'] },
{ name: 'Bob', tags: ['Retail'] }
];
const allTags = customers.flatMap(c => c.tags); // ['VIP', 'Tech', 'Retail']
8. Advanced JavaScript Object & Memory Concepts (6-7+ YOE)
8.1 JavaScript Proxy & Reflect (How Framework Reactivity Works)
Real-Life Scenario: Building custom reactive stores or intercepting property reads/writes dynamically (Vue 3, MobX, Signals).
const userState = { name: 'Alice', age: 30 };
const reactiveUser = new Proxy(userState, {
get(target, prop, receiver) {
console.log(`READ: ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`SET: ${String(prop)} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
reactiveUser.name = 'Bob'; // Triggers SET trap automatically!
8.2 Deep Copying Objects (structuredClone vs JSON.parse vs Spread)
Real-Life Scenario: Safely cloning nested state objects containing Dates, Maps, Sets, and Circular references.
const complexObject = {
id: 101,
date: new Date(),
map: new Map([['key', 'value']])
};
// Native Deep Clone (Supports Date, Map, Set, Array, Objects):
const deepCopy = structuredClone(complexObject);
8.3 WeakMap & WeakSet (Garbage Collection Safety)
Real-Life Scenario: Storing private metadata for DOM elements or component instances without causing memory leaks.
let domNode = document.createElement('div');
const nodeMetadata = new WeakMap();
nodeMetadata.set(domNode, { clickedTimes: 5 });
domNode = null; // domNode is Garbage Collected & WeakMap entry is automatically deleted!
9. Advanced JavaScript Function & Async Concepts (6-7+ YOE)
9.1 Custom Debounce & Throttle Implementation from Scratch
Real-Life Scenario: Writing custom utility functions in interviews without third-party libraries (lodash).
// 1. Debounce implementation from scratch
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn.apply(this, args), delay);
};
}
// 2. Throttle implementation from scratch
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
9.2 Memoization Pattern (Function Result Caching)
Real-Life Scenario: Caching heavy computational results to prevent duplicate CPU calculations.
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
9.3 Currying & Function Composition (In-Depth Masterclass)
PART A: Currying Masterclass
Currying is a functional programming technique where a function with multiple arguments f(a, b, c) is transformed into a chain of unary functions taking 1 argument at a time: f(a)(b)(c).
Why Currying is Useful in Real-Life Software Engineering: Enables Partial Application (pre-configuring reusable base logger modules, API request builders, or form validator functions).
// 1. ES6 Curried Logger Function
const curriedLog = level => moduleName => message => {
console.log(`[${level}] [${moduleName}]: ${message}`);
};
const logError = curriedLog('ERROR');
const logOfferError = logError('OfferService');
logOfferError('Database timeout!'); // [ERROR] [OfferService]: Database timeout!
logOfferError('Invalid payload!'); // [ERROR] [OfferService]: Invalid payload!
// 2. Generic curry() Polyfill Function from Scratch (Senior Interview Live Coding Task)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
function add(a, b, c) { return a + b + c; }
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
PART B: Function Composition (pipe vs compose)
Function Composition is combining simple functions to create a complex pipeline where the output of one function becomes the input to the next: h(x) = f(g(x)).
| Pattern | Execution Direction | How it Works |
|---|---|---|
compose(f, g, h)(x) |
Right-to-Left ⬅️ | Mathematical order: f(g(h(x))) |
pipe(h, g, f)(x) |
Left-to-Right ➡️ | Intuitive pipeline order: x -> h -> g -> f (Used in RxJS .pipe()!) |
// Building pipe() and compose() utilities from scratch:
const pipe = (...fns) => (initialVal) =>
fns.reduce((acc, fn) => fn(acc), initialVal);
const compose = (...fns) => (initialVal) =>
fns.reduceRight((acc, fn) => fn(acc), initialVal);
const trimString = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const wrapInSpan = (str) => `<span>${str}</span>`;
const formatText = pipe(
trimString,
toLowerCase,
wrapInSpan
);
console.log(formatText(' Hello Senior Angular Engineer! '));
// Outputs: "<span>hello senior angular engineer!</span>"
10. Angular Performance & Architecture (6-7+ YOE)
10.1 Zone.js vs Zoneless Angular Architecture
Real-Life Scenario: Eliminating Zone.js dirty-checking bottlenecks for high-performance applications.
// main.ts — Enabling Zoneless Angular (Angular 18+)
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection()
]
});
10.2 ChangeDetectionStrategy.OnPush & Manual Control
Real-Life Scenario: Optimizing large table lists or dashboard grids by skipping unchanged child component subtrees.
@Component({
selector: 'app-user-row',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush // 👈 Skips re-renders unless @Input reference changes or Signal updates!
})
export class UserRowComponent {
private cdr = inject(ChangeDetectorRef);
// markForCheck(): Marks component and parent ancestors for check during next change detection cycle.
onExternalDataArrived() {
this.cdr.markForCheck();
}
// detectChanges(): Forces synchronous change detection on THIS component and its children IMMEDIATELY.
forceImmediateRender() {
this.cdr.detectChanges();
}
}
10.3 Control Flow @for & Deferrable Views (@defer)
Real-Life Scenario: Lazy-loading heavy charts/editor components only when they scroll into the user's viewport.
<!-- 1. Modern @for with mandatory DOM recycling track -->
@for (item of products(); track item.id) {
<div>{{ item.name }}</div>
} @empty {
<p>No products found</p>
}
<!-- 2. Deferrable Views: Heavy chart bundle is NOT downloaded until scrolled into view! -->
@defer (on viewport) {
<app-heavy-chart [data]="salesData()" />
} @placeholder {
<div class="skeleton">Scroll down to load sales chart...</div>
} @loading (minimum 500ms) {
<mat-spinner />
}
10.4 DI Resolution Modifiers (@Optional, @Self, @SkipSelf, @Host)
Real-Life Scenario: Controlling how Angular navigates the ElementInjector tree when building reusable component libraries or dialogs.
| Modifier | Resolution Behavior | Use Case |
|---|---|---|
@Optional() |
Returns null instead of throwing error if dependency is missing. |
Optional logging or theme services |
@Self() |
Looks ONLY at the current element's injector. Does not search parents. | Enforcing element-local directive instances |
@SkipSelf() |
Skips current element's injector and starts searching parent injectors. | Avoiding self-referential tree dependencies |
@Host() |
Searches up the injector tree until reaching the host component template border. | Form control directives binding to host form container |
11. Core JavaScript Engine, Memory & DOM Architecture (6-7+ YOE)
11.1 Garbage Collection & Mark-and-Sweep Algorithm
Real-Life Scenario: Diagnosing and preventing memory leaks in single-page Angular applications.
11.2 Event Bubbling, Capturing & Event Delegation
Real-Life Scenario: Efficiently handling clicks on dynamic lists with 10,000 table rows without attaching 10,000 separate event listeners.
// Event Propagation Order: Capturing Phase (Top -> Down) => Target Phase => Bubbling Phase (Bottom -> Up)
// Event Delegation Pattern: Attach 1 listener to parent container instead of 10,000 children!
const tableBody = document.querySelector('#table-body');
tableBody.addEventListener('click', (event) => {
const targetCell = event.target.closest('.cell-action');
if (targetCell) {
const rowId = targetCell.dataset.id;
console.log(`Clicked action for row ID: ${rowId}`);
}
});
11.3 Generators & Iterators (function* & yield) Explained
Standard JavaScript functions run to completion once called. A Generator Function (function*) is a special function that can pause execution at any yield statement and resume later when .next() is invoked.
1. Iterators & The Iteration Protocol
An Iterator is an object with a .next() method returning { value: any, done: boolean }. Any object implementing [Symbol.iterator]() is an Iterable (Arrays, Strings, Maps, Sets can be looped via for...of).
2. Basic Generator Code Example
function* numberGenerator() {
console.log('Start');
yield 10;
console.log('Resumed #1');
yield 20;
console.log('Resumed #2');
return 30;
}
const gen = numberGenerator();
console.log(gen.next()); // Logs 'Start' -> Returns { value: 10, done: false }
console.log(gen.next()); // Logs 'Resumed #1' -> Returns { value: 20, done: false }
console.log(gen.next()); // Logs 'Resumed #2' -> Returns { value: 30, done: true }
3. Two-Way Communication: Passing Values BACK into Generator via gen.next(val)
yield is a two-way street! You can yield a value OUT, and pass a value BACK IN when unpausing.
function* chatBot() {
const name = yield 'What is your name?';
console.log(`Hello, ${name}!`);
const age = yield 'How old are you?';
console.log(`You are ${age} years old.`);
}
const chat = chatBot();
console.log(chat.next().value); // Yields: 'What is your name?'
console.log(chat.next('Alice').value); // Passes 'Alice' -> Yields: 'How old are you?'
chat.next(28); // Passes 28 -> Logs: 'You are 28 years old.'
4. Senior Real-Life Scenarios for Generators
| Scenario | How Generators Solve It |
|---|---|
| Infinite Sequence Generators | Generates unique transaction IDs or Fibonacci numbers infinitely without storing millions of items in memory RAM. |
| Paginated Async Data Streams | Using async function* and for await...of to fetch REST API pages on-demand chunk by chunk. |
| State Machine Control Flow | Managing step-by-step wizard forms or redux-saga task orchestration. |
5. Async Generator Example (Streaming REST API Pages)
async function* fetchPaginatedUsers() {
let page = 1;
let hasMore = true;
while (hasMore) {
const res = await fetch(`/api/users?page=${page}`);
const data = await res.json();
yield data.items;
hasMore = page < data.totalPages;
page++;
}
}
async function loadAllUsers() {
for await (const userChunk of fetchPaginatedUsers()) {
console.log('Loaded chunk of users:', userChunk);
}
}
6. Creating Custom Iterables using Symbol.iterator
const team = {
members: ['Alice', 'Bob', 'Charlie'],
*[Symbol.iterator]() {
for (const member of this.members) {
yield member;
}
}
};
for (const person of team) {
console.log(person);
}
console.log([...team]);
12. Custom RxJS Operators & Schedulers (6-7+ YOE)
12.1 Building a Custom Pipeable RxJS Operator from Scratch
Real-Life Scenario: Writing clean reusable custom RxJS operators (e.g., filtering out null/undefined values or auto-logging errors).
import { Observable } from 'rxjs';
export function filterNil<T>() {
return (source: Observable<T | null | undefined>): Observable<T> => {
return new Observable<T>((subscriber) => {
return source.subscribe({
next(value) {
if (value !== null && value !== undefined) {
subscriber.next(value);
}
},
error(err) { subscriber.error(err); },
complete() { subscriber.complete(); }
});
});
};
}
this.user$.pipe(
filterNil()
).subscribe(user => console.log(user.name));
12.2 RxJS Schedulers Architecture
Real-Life Scenario: Controlling thread execution queues (Microtask vs Macrotask vs Animation Frame) inside RxJS pipelines.
| Scheduler | Execution Queue | Best Used For |
|---|---|---|
queueScheduler |
Synchronous execution (Queue) | Iterating over arrays synchronously without stack overflow |
asapScheduler |
Microtask Queue (Promise resolution) | Operations that should run immediately after current stack empties |
asyncScheduler |
Macrotask Queue (setTimeout) |
Time-based operations like interval, delay, or debouncing |
animationFrameScheduler |
requestAnimationFrame |
Smooth 60fps UI animations & canvas redraws |
13. Advanced TypeScript Masterclass (6-7+ YOE Technical Interviews)
13.1 any vs unknown vs never vs void (Top Senior Interview Question)
Real-Life Scenario: Enforcing absolute type-safety across enterprise APIs and error handling handlers.
| Type | Type-Checking strictness | Can be assigned to anything? | Can call methods on it directly? | Best Used For |
|---|---|---|---|---|
any |
Disables type checking completely ❌ | Yes | Yes (Unsafe!) | Legacy JS migration only (Avoid in production) |
unknown |
Safe Top Type ✅ (Requires type narrowing before use) | Yes | No (Must check type first via if (typeof x === 'string')) |
API responses, dynamic JSON parsing, third-party input |
never |
Bottom Type (Represents a value that NEVER occurs) | No | No | Functions that throw errors, infinite loops, switch exhaustiveness checks |
void |
Represents absence of return value | No | No | Functions returning nothing |
// 1. unknown requires narrowing:
function processApiInput(data: unknown) {
if (typeof data === 'string') {
console.log(data.toUpperCase()); // ✅ Safe! TypeScript knows it's a string!
}
}
// 2. never for exhaustiveness checking in switch statements:
function assertNever(x: never): never {
throw new Error(`Unexpected object: ${x}`);
}
13.2 Essential Built-in Utility Types Masterclass
Real-Life Scenario: Transforming existing interface DTOs into form models, partial updates, or read-only states without code duplication.
| Utility Type | Behavior | Real-Life Code Example |
|---|---|---|
Partial<T> |
Makes all properties optional (?) |
type UpdateUserDto = Partial<User>; (For PATCH requests) |
Required<T> |
Makes all properties required | type CompleteUser = Required<UserDraft>; |
Readonly<T> |
Makes all properties immutable (readonly) |
type ConfigState = Readonly<AppConfig>; |
Record<K, T> |
Constructs an object type with keys K and values T | type UserMap = Record<number, User>; (Dictionary lookup) |
Pick<T, K> |
Selects only specific keys K from T | type UserSummary = Pick<User, 'id' | 'name'>; |
Omit<T, K> |
Removes specific keys K from T | type CreateUserPayload = Omit<User, 'id' | 'createdAt'>; |
NonNullable<T> |
Excludes null and undefined from T |
type CleanString = NonNullable<string | null | undefined>; |
ReturnType<T> |
Extracts return type of a function T | type ApiResponse = ReturnType<typeof fetchUser>; |
13.3 Generics & keyof Constraints (generic <T extends Constraint>)
Real-Life Scenario: Writing strongly typed helper functions that enforce property key existence at compile time.
// Function guarantees 'key' MUST exist on object 'obj'!
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 101, username: 'Alice', role: 'Admin' };
const username = getProperty(user, 'username'); // ✅ Type inferred as string!
// const invalid = getProperty(user, 'email'); ❌ Compile Error: Argument of type '"email"' is not assignable to keyof user!
13.4 Custom User-Defined Type Guards (is Keyword)
Real-Life Scenario: Safely narrowing untyped API payloads into concrete TypeScript interfaces at runtime.
interface AdminUser {
id: number;
adminPermissions: string[];
}
// Custom Type Guard returning "user is AdminUser"
function isAdmin(user: any): user is AdminUser {
return user && Array.isArray(user.adminPermissions);
}
function handleUser(user: any) {
if (isAdmin(user)) {
// TypeScript compiler automatically narrows 'user' to AdminUser inside this block!
console.log(user.adminPermissions.join(', ')); // ✅ Autocomplete works!
}
}
13.5 Discriminated Unions & Exhaustiveness Checks
Real-Life Scenario: Managing complex UI state machines (Loading, Success, Error) with 100% type safety.
type ApiResponse<T> =
| { status: 'LOADING' }
| { status: 'SUCCESS'; data: T }
| { status: 'ERROR'; errorMessage: string };
function renderState<T>(state: ApiResponse<T>) {
switch (state.status) {
case 'LOADING':
return 'Spinner...';
case 'SUCCESS':
return `Data: ${JSON.stringify(state.data)}`; // ✅ TypeScript knows state.data exists here!
case 'ERROR':
return `Error: ${state.errorMessage}`; // ✅ TypeScript knows state.errorMessage exists here!
}
}
13.6 Conditional Types & infer Keyword
Real-Life Scenario: Unwrapping asynchronous Promises or Array types inside generic utility libraries.
// 1. Unwrapping Array element types using infer
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type StringArray = string[];
type SingleString = UnpackArray<StringArray>; // Inferred as string
// 2. Unwrapping Promise return value using infer
type UnpackPromise<T> = T extends Promise<infer R> ? R : T;
type AsyncData = Promise<{ id: number; name: string }>;
type ResolvedData = UnpackPromise<AsyncData>; // Inferred as { id: number; name: string }
13.7 Mapped Types & Template Literal Types
Real-Life Scenario: Auto-generating getter method types or strongly-typed event strings dynamically.
// 1. Template Literal Types for Event Listeners
type EventType = 'click' | 'hover';
type ElementTarget = 'button' | 'link';
type EventName = `${EventType}_${ElementTarget}`;
// Resulting Type: 'click_button' | 'click_link' | 'hover_button' | 'hover_link'
// 2. Mapped Type to auto-generate Getter signature types
type Person = { name: string; age: number };
type PersonGetters = {
[K in keyof Person as `get${Capitalize<string & K>}`]: () => Person[K]
};
// Resulting Type: { getName: () => string; getAge: () => number }
Post a Comment