JS & TypeScript Quick Recall
Last-minute cheatsheet — scan in 5–10 minutes before your interview. Key concepts, gotchas, and code snippets.
Closures & Scope
Closure
- A function that remembers the variables from where it was created , even after that outer function has returned.
- You don't opt in , every function automatically captures its surrounding scope ("lexical environment").
- What it buys you: private state, factory functions, memoization caches, partial application.
const make = x => y => x + y; // closes over x const add5 = make(5); add5(3); // 8
Warning
Watch out
The loop trap: a var loop variable is ONE shared variable, so every async callback sees its final value. Declare it with let (fresh per iteration) instead.
Scope types
- Global scope , declared outside everything, visible everywhere.
- Function scope , var and parameters belong to the whole function, ignoring blocks.
- Block scope , let / const live only inside the nearest { }.
- Lexical scope , what a function can see is fixed by WHERE it is written, not where it is called from.
Hoisting
var vs let/const vs function declarations
- var , registered AND initialised to undefined at the top of its scope. Read it before its line and you get undefined, not an error.
- let / const , registered but locked until their line (the Temporal Dead Zone). Touching them early throws a ReferenceError.
- function declaration , hoisted whole, body included. Callable from lines above where it is written.
- function expression (const fn = () => {}) , it's just a variable holding a function, so only the variable name hoists, not the function.
console.log(a); // undefined (var)
console.log(b); // ReferenceError (let TDZ)
foo(); // works — function declaration
var a = 1;
let b = 2;
function foo() {}Warning
Watch out
You can tell a TDZ error apart: it says "Cannot access 'b' before initialization" , a truly missing variable says "b is not defined".
this Keyword
4 binding rules (in priority order)
- 1. new binding , new Fn() → this is the freshly created object.
- 2. Explicit binding , call/apply/bind → this is whatever object you passed in.
- 3. Implicit binding , obj.method() → this is obj, the thing left of the dot.
- 4. Default binding , a bare fn() → this is globalThis (or undefined in strict mode).
Arrow functions
- Arrow functions have NO this of their own.
- They simply use the this of the surrounding code, captured where they are DEFINED , and nothing can rebind it.
- They can't be constructors , calling one with new throws.
- That capture is exactly why they're perfect for callbacks inside methods (the setInterval example below).
class Timer {
seconds = 0;
start() {
setInterval(() => this.seconds++, 1000); // arrow — inherits 'this'
}
}Warning
Watch out
Don't write object methods as arrows: obj.method = () => {} does NOT get obj as this , it inherits from outside the object (often the global scope).
Event Loop
Execution order
- 1. All synchronous code runs first, to completion, on the call stack.
- 2. When the stack empties, the ENTIRE microtask queue is drained.
- 3. Then ONE macrotask runs , and the cycle repeats.
- Microtasks (the urgent queue): Promise .then/.catch, code after await, queueMicrotask, MutationObserver.
- Macrotasks (the regular queue): setTimeout, setInterval, I/O callbacks, UI events.
console.log('1'); // sync
setTimeout(() => console.log('4'), 0); // macrotask
Promise.resolve().then(() => console.log('3')); // microtask
console.log('2'); // sync
// Output: 1, 2, 3, 4Warning
Watch out
A promise callback ALWAYS beats setTimeout(fn, 0) queued at the same time , microtasks jump the queue.
Promises & async/await
Promise states
- pending → fulfilled (success) or rejected (failure) , and once settled, frozen forever.
- .then(onFulfilled, onRejected) returns a NEW promise , that's what makes chains possible.
- .catch(fn) is just shorthand for .then(undefined, fn) , it catches failures from anywhere earlier in the chain.
- .finally(fn) runs either way and receives nothing , the place for cleanup like hiding a spinner.
Promise combinators
- Promise.all([...]) , everything must succeed; the FIRST failure rejects the whole batch.
- Promise.allSettled([...]) , waits for all, never rejects; gives a {status, value|reason} report per promise.
- Promise.race([...]) , the FIRST to settle wins, success or failure (great for adding timeouts).
- Promise.any([...]) , the first SUCCESS wins; only fails if all fail (AggregateError).
// Parallel fetches (don't await sequentially!) const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);
async/await gotchas
- An async function ALWAYS returns a promise , even a plain return value comes wrapped.
- await pauses only its own function , everything outside keeps running.
- Independent tasks awaited one after another run one after another , start them together with Promise.all.
- A rejected await throws , wrap in try/catch, or attach .catch() to the returned promise.
- Never await inside forEach , it fires all the callbacks without waiting. Use for...of, or Promise.all(arr.map(async ...)).
Warning
Watch out
await inside forEach is silently broken: forEach ignores the promises your callback returns, so nothing actually waits.
ES6+ Essentials
Destructuring
- Objects: const { a, b: alias, c = fallback } = obj , pick, rename, and default in one line.
- Arrays: const [first, , third, ...rest] = arr , pick by position, skip with a gap, collect the tail.
- Nested data in one go: const { user: { address: { city } } } = data.
- Straight into function parameters: function fn({ id, name = "anon" }) {}.
Spread & Rest
- Spread arrays: [...arr1, ...arr2] , copy or merge in one expression.
- Spread objects: { ...obj1, ...obj2 } , merged; when keys collide, the later one wins.
- Rest params: function fn(a, ...rest) , the extras arrive as a genuine Array.
- Both copy ONE level deep only , nested objects inside are still shared with the original.
Warning
Watch out
Spread is a shallow clone: the copy has the same nested objects as the original. Mutate one, you've mutated both.
?. and ??
- obj?.prop , undefined instead of a crash when obj is null/undefined.
- arr?.[0] , the same safety for index access.
- fn?.() , call it only if it exists.
- a ?? b , fall back ONLY when a is truly missing (null/undefined) , 0 and "" survive.
- a || b , falls back on ANY falsy value, replacing legitimate 0, "", and false too.
Warning
Watch out
0 ?? "default" gives 0, but 0 || "default" gives "default" , use ?? whenever 0 or "" are values you want to keep.
Array Methods
Transforming (returns new array)
- map(fn) , transform every element; same length out as in.
- filter(fn) , keep only the elements where fn returns true.
- flatMap(fn) , map, then flatten one level , perfect when each item can produce zero, one, or many results.
- flat(depth) , lift nested arrays up into the parent, one level by default.
[1,2,3].map(x => x * 2); // [2,4,6] [1,2,3,4].filter(x => x % 2 === 0); // [2,4] [[1,2],[3,4]].flat(); // [1,2,3,4]
Aggregating (returns single value)
- reduce(fn, init) , fold the array into anything: a sum, an object, another array.
- find(fn) , the first element that passes the test, or undefined.
- findIndex(fn) , its position instead, or -1.
- some(fn) , "does at least one pass?" , stops at the first yes.
- every(fn) , "do they all pass?" , stops at the first no.
- includes(val) , is this value in the array? (and unlike indexOf, it can find NaN).
Sorting & mutating (mutates original!)
- sort((a,b) => a - b) , sorts IN PLACE, and without that comparator it sorts numbers as text ([10, 2, 1] → [1, 10, 2]).
- reverse() , also flips the array in place.
- splice(i, n, ...items) , surgically remove/insert in place.
- Want the original untouched? Copy first , [...arr].sort(comparator) , or use the ES2023 copies: toSorted / toReversed / toSpliced.
Warning
Watch out
sort() changes the original array , a classic React state bug. Spread first: [...arr].sort().
Prototype & Classes
Prototype chain
- Every object holds a hidden link ([[Prototype]]) to another object, forming a chain that ends at null.
- Property lookup walks that chain: own properties → prototype → its prototype → ... → null.
- Object.create(proto) builds an object with exactly the prototype you choose.
- Read the link with Object.getPrototypeOf(obj) , the __proto__ accessor works but is the legacy spelling.
ES6 Classes
- Cleaner syntax over the same prototype system , not a new object model.
- constructor() runs on new ClassName() , per-instance setup goes here.
- In a subclass constructor, super() must run before you touch this.
- static members live on the class itself, not on instances.
- #field is genuinely private , inaccessible outside the class body, not just a naming convention.
- Methods in the class body are stored once on the prototype and shared by every instance.
class Animal {
#name;
constructor(n) { this.#name = n; }
speak() { return this.#name; }
}
class Dog extends Animal {
speak() { return super.speak() + ': woof'; }
}Common Gotchas
Type coercion traps
- typeof null → "object" , a bug from 1995, kept forever. Check with value === null.
- typeof [] → "object" too , arrays need Array.isArray().
- NaN !== NaN , the one value not equal to itself. Check with Number.isNaN().
- The party tricks: [] + [] → "" · [] + {} → "[object Object]" · {} + [] → 0 (the {} parses as an empty block!).
- Always give parseInt its radix , parseInt("08", 10) , so the base is never guessed.
Warning
Watch out
Default to === everywhere. Loose == coercion rules are genuinely hard to memorise and breed subtle bugs.
Pass by value vs reference
- Primitives (number, string, boolean, null, undefined, symbol, bigint) are copied , the function gets its own value.
- Objects and arrays pass a copy of the REFERENCE , both variables point at the same underlying object.
- So mutating an object parameter inside a function mutates the caller's object too.
- Defend by copying: spread/Object.assign for one level, structuredClone for a full deep clone.
TypeScript Essentials
type vs interface
- interface , open: extendable with extends, and re-declarable to add members (declaration merging).
- type , closed (no merging), but it can name ANYTHING: unions, intersections, primitives, tuples.
- Reach for interface when describing object shapes and public APIs.
- Reach for type when you need unions, intersections, or computed/mapped types.
Generics
- <T> declares a type placeholder that gets filled in where the function/type is used.
- Constrain what's allowed: <T extends SomeType> , "any T, as long as it fits SomeType".
- Give it a default: <T = string>.
- Combine them: <T, K extends keyof T> , "K must be a real key of T" (the typed-lookup pattern below).
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}Key Utility Types
- Partial<T> , every property becomes optional (perfect for update payloads).
- Required<T> , the reverse: everything mandatory.
- Pick<T, K> , a slimmed-down type keeping only the K keys.
- Omit<T, K> , the complement: everything EXCEPT the K keys.
- Record<K, V> , an object type with keys K and values V (a typed dictionary).
- ReturnType<typeof fn> , extract what a function returns, no manual retyping.
- NonNullable<T> , T with null and undefined stripped out.
Type Guards
- typeof x === "string" , narrows x to string inside the branch.
- x instanceof Date , narrows to Date.
- "prop" in obj , narrows to the union member that has that property.
- Custom guards: function isUser(x): x is User { ... } , your own check, trusted by the compiler.
- Discriminated unions: switch on a literal "kind"/"type" field and each case narrows automatically.
satisfies
- expr satisfies Type checks expr against Type WITHOUT widening it , unlike a `:` annotation (which widens) or `as` (which skips checking entirely).
- You get both: a compile error if a key is wrong, and the narrowest inferred type afterwards for every value.
- The classic use case: a config/palette object where each individual value should stay as specific as possible after validation.
const palette = { red: [255, 0, 0], green: '#0f0' } satisfies Record<string, string | number[]>;
palette.green.toUpperCase(); // still known as stringTemplate Literal Types
- Build new string literal types the same way you build template strings at runtime , interpolating a literal type into a backtick-delimited type produces a new, more specific string literal type.
- Interpolating a union produces the cross-product of every combination , great for deriving event-name types from an object's own keys.
- Ships with Uppercase<T>, Lowercase<T>, Capitalize<T>, Uncapitalize<T> built in.
using / await using
- A const-like declaration that auto-runs cleanup ([Symbol.dispose]()) the moment it leaves scope , on a normal exit, an early return, or a thrown error, no try/finally needed.
- await using is the async version, for AsyncDisposable ([Symbol.asyncDispose]()).
- Resources dispose in last-in-first-out order, like a stack.
in / out Variance
- out T (covariant): T only in output/return positions , a Getter<Dog> can stand in for a Getter<Animal>.
- in T (contravariant): T only in input/parameter positions , the substitution direction flips.
- The compiler verifies the annotation is actually true , it's not just documentation.
import type & Awaited<T>
- import type { Foo } from "./mod" guarantees the import is fully erased at compile time , zero runtime trace, needed by single-file transpilers like esbuild/SWC.
- Awaited<Promise<Promise<number>>> resolves to number , it recursively unwraps nested Promises, mirroring what repeated await calls actually do at runtime.
- NoInfer<T> (TS 5.4+) blocks a parameter from influencing generic inference , useful for a "default value" argument that shouldn't widen the inferred type.