Interview Prep
Problems modelled on what technical interviews ask. Solve them in JavaScript, TypeScript, Python or SQL.
Difficulty
Topics
Language
Callback hell makes code impossible to follow and debug. Refactor solve(userData, ordersData, productsData) using async/await so the function is flat and readable. The fake fetchers are already provided as Promises — no callbacks. solve({ id: 'u1', name: 'Alice' }, [{ id: 'o1' }], [{ id: 'p1' }]) → Promise<{ user, orders, products }>
jest.useFakeTimers() replaces the real clock with one the test controls: timers only fire when the test advances time. solve(ops) runs a list of operations against a fresh fake clock (starting at 0) and returns their results: | Op | Result | |---|---| | ['setTimeout', label, delay] | timer ID (0, 1, …). The timer is due at now + delay. | | ['clearTimeout', id] | true if a pending timer was cancelled, false otherwise (unknown, already fired or already cleared) | | ['advance', ms] | moves the clock forward by ms, fires every due timer, returns their labels in firing order | | ['runAll'] | fires every pending timer, returns their labels; the clock ends at the last fired timer's due time | | ['pending'] | number of timers still waiting | | ['now'] | current fake time | Timers fire in order of due time; timers due at the same time fire in creation order.
Magic numbers make code unreadable. What does 86400000 mean? What about 0.2? Refactor by introducing named constants so the intent is self-documenting. The output must remain identical.
Functions with many parameters are hard to call correctly — which order do the args go? Refactor so solve accepts a single options object instead of 5 positional arguments. solve({ firstName: 'Alice', lastName: 'Smith', age: 30, email: 'a@b.com', role: 'admin' })
Silently swallowing errors hides bugs and makes debugging a nightmare. Refactor solve(json) so failure is never silent: on success return { ok: true, value: <parsed> }, on failure return { ok: false, error: 'Invalid JSON' } — never a bare null that hides which case happened.
The function below does too many things at once — it validates, transforms and formats all in one block. Refactor solve(users) by extracting helper functions so each function does exactly one thing. The output must be identical. Your extracted version should have at minimum: isValid(user), formatUser(user), and the main solve(users) that composes them.
Complex boolean expressions in if statements hide intent. Refactor by extracting the condition into a named predicate isEligible(user). solve(user, 'isEligible') → true/false · solve(user, 'getStatus') → 'eligible'/'not-eligible'
Deep nesting is hard to read. Replace nested if blocks with guard clauses that return early. Refactor using guard clauses so the happy path is at the end with minimal nesting.
The functions below share ~80% of their logic. Any change to the formatting must be made in 3 places. Refactor by extracting a generic formatCurrency(amount, symbol) helper. Then solve(amount, type) dispatches to the right formatter — 'formatUSD', 'formatEUR', or 'formatGBP'.
A function should either do something (command) or answer something (query) — never both. Refactor into two separate functions: push(stack, value) — command: pushes value, returns null/void size(stack) — query: returns the current length, no mutation solve(stack, value, 'push') dispatches to push · solve(stack, 'size') dispatches to size.
The function below mutates its input array — a classic source of bugs. Refactor solve(cart, item) so it returns a new sorted array without modifying cart. After calling solve(cart, item), the original cart array must be unchanged.
A boolean flag argument is a code smell — it means the function does two things. Split this into two functions: createUser(name) and createAdminUser(name). Then solve(name, type) dispatches to the right one — no boolean flag. solve('Alice', 'createUser') → { name: 'Alice', role: 'user', permissions: ['read'] } solve('Bob', 'createAdminUser') → { name: 'Bob', role: 'admin', permissions: ['read', 'write', 'delete'] }
Implement solve(n) that returns a list of strings for numbers from 1 to n (inclusive) following these rules: "FizzBuzz" if divisible by both 3 and 5 "Fizz" if divisible by 3 only "Buzz" if divisible by 5 only The number as a string otherwise Return an empty list if n < 1 — there is nothing to count. Examples solve(5) → ["1", "2", "Fizz", "4", "Buzz"] solve(15)[-1] → "FizzBuzz"
Implement solve(a, b) that: Returns a / b (float division) if b != 0. Returns None if b == 0 (catch the ZeroDivisionError). Raises a TypeError with message "Inputs must be numbers" if either argument is not an int or float. Examples solve(10, 2) → 5.0 solve(7, 0) → None solve("a", 2) → raises TypeError("Inputs must be numbers") In JavaScript / TypeScript return null for a zero divisor and throw new TypeError('Inputs must be numbers'). Note that JavaScript never throws on division by zero — 7 / 0 is Infinity — so the zero case is a guard, not a catch.
Jest's expect(fn).toThrow() does not receive a value — it receives a function, calls it, and checks whether it throws. The starter code defines a few functions under test in SAMPLES. Implement the two matchers: | Matcher | Passes when | |---|---| | toThrow(fn) | fn() throws anything | | toThrow(fn, expected) | fn() throws and the error message contains expected | | notToThrow(fn) | fn() does not throw | solve(name, matcher, expected?) looks up SAMPLES[name] and runs 'toThrow' or 'not.toThrow' on it. Return { pass, message } — message is '' when the matcher passes, otherwise one of: "Expected function to throw but it did not" "Expected error message to contain 'oops' but got 'boom'" "Expected function not to throw but it threw: oops"
Implement a lightweight version of Jest's expect() API. solve(matcher, received, expected?) must support these matchers: | Matcher | Description | |---|---| | 'toBe' | Strict equality (===). expected required. | | 'toEqual' | Deep equality (nested objects/arrays). expected required. | | 'toBeTruthy' | Passes if received is truthy. | | 'toBeFalsy' | Passes if received is falsy. | | 'toBeNull' | Passes if received === null. | | 'toBeUndefined' | Passes if received === undefined. | | 'toBeDefined' | Passes if received !== undefined. | Return an object { pass: boolean, message: string }. When passing, message is ''. When failing, message is a human-readable explanation, e.g.: "Expected 5 but received 3" "Expected value to be truthy but received false"
Either represents a value that is one of two things: a Right (success) or a Left (failure/error). Unlike Maybe, Left carries information about what went wrong. Implement Right and Left classes, each with: static of(value) .map(fn) — Right applies fn, Left ignores it .fold(leftFn, rightFn) — calls the appropriate function
This function does three distinct jobs: fetches, filters, and formats — a maintenance nightmare. Refactor into three functions, each with one job: filterActive(users) — keep only active users filterSeniors(users) — keep only users aged 60+ format(users) — format as "NAME (age)" strings solve(users, 'filterActive') dispatches to the right function.
Software entities should be open for extension, closed for modification. Adding a new case should not require editing existing code. Refactor so each shape has its own area() method. Create createCircle(r), createRect(w, h), createTriangle(b, h). Then solve(type, ...dims) just instantiates the right shape and calls .area() — no if chains needed when adding new shapes.
Defensive null checks scattered throughout code are a smell. Refactor using a Null Object: create a GUEST_USER constant that provides safe defaults, then use it when user is falsy. solve(null) → { name: 'Anonymous', email: 'no-email@placeholder.com', role: 'guest' }
Method chaining creates readable, declarative code. Each method returns this to allow chaining. Implement createPipeline(items) that returns a fluent builder with filter(fn), map(fn), take(n) and value() methods — all chainable. createPipeline([1,-2,3,4,5]).filter(x=>x>0).map(x=>x*2).take(2).value() → [2, 6]
Subtypes must be substitutable for their base type without breaking behavior. Refactor using factory functions so createRectangle and createSquare are independent — no inheritance. Both expose area(). solve('square', 5) → 25 (side² ) solve('rect', 4, 6) → 24 (width × height)
Deep inheritance chains are brittle. When requirements change, you end up fighting the hierarchy. Refactor using composition: create createAnimal(name, sound) that returns an object with a speak() method. No classes, no inheritance. solve(name, sound) calls createAnimal(name, sound).speak(). solve('Rex', 'woof') → 'Rex says: woof'
High-level modules should not depend on low-level modules — both should depend on abstractions. Refactor solve(db, id): instead of hardcoding the repo, create a simple in-memory repo from db (a plain object) and return db[id]. The point: solve receives its data source — it doesn't instantiate one. solve({ u1: { id: 'u1', name: 'Alice' } }, 'u1') → { id: 'u1', name: 'Alice' }
Implement solve(plan), a tiny describe / it runner. The starter code provides the hooks and test bodies in HOOKS and TESTS; the plan refers to them by name: Rules: Each suite gets a fresh, empty context object ctx, shared by all of its hooks and tests. For each test: run beforeEach(ctx), then the test body fn(ctx), then afterEach(ctx). A test passes only if its body returns exactly true. Otherwise it fails with an error: beforeEach threw → its message (the body is skipped, afterEach still runs) the body threw → its message the body returned anything but true → 'Test did not return true' afterEach threw after a passing body → its message Return { passed, failed, results }, where results lists { suite, test, pass, error } in execution order (error is null for a passing test).
A spy is a fake function that records every call made to it. Implement one. solve(ops) receives a list of operations, runs them in order against a fresh set of spies, and returns the list of their results: | Op | Args | Result | |---|---|---| | ['create', returnValue?] | | new spy ID (0, 1, …). Calling the spy returns returnValue, or null if none was given. | | ['call', id, ...args] | | the spy's current return value. Records the call. | | ['getCalls', id] | | every call so far: [{ args, returnValue }] | | ['getCallCount', id] | | number of recorded calls | | ['mockReturnValue', id, value] | | null — future calls return value | | ['reset', id] | | null — clears the call history, keeps the return value |
A snapshot test serializes a value, stores the string, and fails later if the serialization changes. solve(ops) runs a list of operations against an empty snapshot store and returns their results: | Op | Result | |---|---| | ['serialize', value] | the value's snapshot string (see format below) | | ['save', name, value] | 'saved' — stores (or overwrites) the snapshot name | | ['match', name, value] | { pass: true, diff: null } if value serializes to the stored snapshot, else { pass: false, diff: { expected, received } } (expected is null when no snapshot exists) | | ['has', name] | whether a snapshot exists | | ['delete', name] | true if a snapshot was deleted, false if there was none | Format Primitives (numbers, strings, booleans, null) are serialized like JSON.stringify — 42, "hello", true, null. Empty arrays / objects are [] / {}. Otherwise one item per line, indented by 2 spaces per level; object keys are sorted and written as "key": value:
Jest lets you register custom matchers with expect.extend({ ... }), then use them like built-ins — including through .not. The starter code provides the custom matchers in MATCHER_LIBRARY. Each takes (received, ...args) and returns { pass, message }, where message describes the failure. solve(ops) runs a list of operations in order, starting from an empty registry, and returns their results: | Op | Result | |---|---| | ['extend', names] | null — registers the named matchers from MATCHER_LIBRARY | | ['match', name, received, ...args] | { pass, message } — runs a registered matcher | | ['list'] | registered matcher names, in registration order, without duplicates | Rules for match: message is '' whenever the result passes. A name prefixed with not. inverts the matcher. If the inverted check fails, the message is 'toBeEven unexpectedly passed for 4'. A name that was never registered → { pass: false, message: 'Unknown matcher: toBeOdd' }.
Coverage tools work in two phases: they instrument the source (insert a probe before every line), then, after the tests ran, turn the recorded hits into a report. Implement both, plus merging the hits of several test runs. solve(op, ...args): | Op | Args | Result | |---|---|---| | 'instrument' | (sourceLines) | the lines with a probe in front: line i becomes ` __hit(i); ${line} | | 'merge' | (covA, covB) | the union of two coverage maps | | 'report' | (sourceLines, cov) | { total, covered, percent, uncovered } | A coverage map is an object whose keys are line indices (as strings, since it is JSON) and whose values are true: { "0": true, "2": true } means lines 0 and 2 ran. Report rules: total is the number of source lines; covered counts the lines of the source present in cov (keys outside the source are ignored). percent is covered / total * 100 rounded half up (12.5 → 13), and 100 for an empty file. uncovered` lists the 0-based indices of the lines that never ran, ascending.