Coding Interview Questions

Implement DeepPartial and DeepReadonly.

Example

Recurse into object properties; treat arrays separately for DeepReadonly so they become readonly arrays.

type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
type DeepReadonly<T> =
  T extends (...args: any[]) => any ? T
  : T extends readonly (infer E)[] ? readonly DeepReadonly<E>[]
  : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type Config = { db: { host: string; port: number }; features: { flags: string[] } };

function deepMerge<T extends object>(base: T, patch: DeepPartial<T>): T {
  const out = { ...base } as Record<string, unknown>;
  for (const [k, v] of Object.entries(patch)) {
    const cur = out[k];
    out[k] = v && typeof v === "object" && !Array.isArray(v) && cur && typeof cur === "object"
      ? deepMerge(cur, v as DeepPartial<typeof cur>) : v;
  }
  return out as T;
}

const base: Config = { db: { host: "localhost", port: 5432 }, features: { flags: ["a"] } };
const merged: DeepReadonly<Config> = deepMerge(base, { db: { port: 6543 } });
console.log(merged.db, merged.features.flags);
// merged.db.port = 1;          -> error: readonly
// merged.features.flags.push() -> error: readonly array

Output

{ host: 'localhost', port: 6543 } [ 'a' ]

Build a type-safe EventEmitter.

Example

The event map type ties each event name to its payload, so on and emit are checked.

class TypedEmitter<Events extends Record<string, unknown>> {
  private listeners: { [E in keyof Events]?: ((payload: Events[E]) => void)[] } = {};

  on<E extends keyof Events>(event: E, fn: (payload: Events[E]) => void): () => void {
    (this.listeners[event] ??= []).push(fn);
    return () => { this.listeners[event] = this.listeners[event]?.filter((f) => f !== fn); };
  }

  emit<E extends keyof Events>(event: E, payload: Events[E]): void {
    this.listeners[event]?.forEach((fn) => fn(payload));
  }
}

type ShopEvents = {
  "order:placed": { orderId: string; amount: number };
  "order:cancelled": { orderId: string; reason: string };
  "stock:low": { sku: string; left: number };
};

const bus = new TypedEmitter<ShopEvents>();
const off = bus.on("order:placed", (e) => console.log("placed", e.orderId, "Rs.", e.amount));
bus.on("stock:low", (e) => console.log("restock", e.sku, "only", e.left, "left"));

bus.emit("order:placed", { orderId: "OD-1", amount: 1299 });
bus.emit("stock:low", { sku: "SKU-9", left: 2 });
off();
bus.emit("order:placed", { orderId: "OD-2", amount: 99 }); // no listener now
// bus.emit("order:placed", { orderId: "OD-3" }); -> error: amount missing

Output

placed OD-1 Rs. 1299
restock SKU-9 only 2 left

Write a typed groupBy and a type-safe pick.

Example

function groupBy<T, K extends PropertyKey>(items: readonly T[], keyFn: (item: T) => K): Record<K, T[]> {
  const out = {} as Record<K, T[]>;
  for (const item of items) (out[keyFn(item)] ??= []).push(item);
  return out;
}

function pick<T extends object, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) out[k] = obj[k];
  return out;
}

type Order = { id: string; city: "Delhi" | "Pune"; total: number; internal: string };
const orders: Order[] = [
  { id: "1", city: "Delhi", total: 500, internal: "x" },
  { id: "2", city: "Pune", total: 300, internal: "y" },
  { id: "3", city: "Delhi", total: 200, internal: "z" },
];

const byCity = groupBy(orders, (o) => o.city); // Record<"Delhi" | "Pune", Order[]>
console.log(byCity.Delhi.map((o) => o.id), byCity.Pune.length);
console.log(pick(orders[0], "id", "total")); // { id: string; total: number }

Output

[ '1', '3' ] 1
{ id: '1', total: 500 }

Implement a generic debounce that keeps parameter types.

Example

Parameters of the wrapped function become the parameters of the debounced one, so wrong arguments are still compile errors.

function debounce<Args extends unknown[]>(fn: (...args: Args) => void, waitMs: number) {
  let timer: ReturnType<typeof setTimeout> | undefined;
  const debounced = (...args: Args): void => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), waitMs);
  };
  debounced.cancel = () => clearTimeout(timer);
  return debounced;
}

const search = debounce((query: string, page: number) => {
  console.log(`API call: q=${query} page=${page}`);
}, 50);

// user types quickly: only the last call within 50 ms runs
search("s", 1);
search("sh", 1);
search("sho", 1);
setTimeout(() => search("shoes", 2), 100);
// search(42) -> error: number is not assignable to string

Output

API call: q=sho page=1
API call: q=shoes page=2

Implement a Result type with map and a helper that wraps throwing code.

Example

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

function tryCatch<T>(fn: () => T): Result<T> {
  try { return ok(fn()); }
  catch (e) { return err(e instanceof Error ? e : new Error(String(e))); }
}

function map<T, U, E>(r: Result<T, E>, fn: (v: T) => U): Result<U, E> {
  return r.ok ? ok(fn(r.value)) : r;
}

for (const raw of ['{"price": 499}', "{oops"]) {
  const parsed = tryCatch(() => JSON.parse(raw) as { price: number });
  const withGst = map(parsed, (p) => Math.round(p.price * 1.18));
  console.log(withGst.ok ? "price with GST: " + withGst.value : "error: " + withGst.error.name);
}

Output

price with GST: 589
error: SyntaxError

Extract route params from a path string at the type level.

Example

A recursive template literal type with infer. The type test compiles only if the result is exactly "shopId" | "productId".

type RouteParams<P extends string> =
  P extends `${string}:${infer Param}/${infer Rest}` ? Param | RouteParams<Rest>
  : P extends `${string}:${infer Param}` ? Param
  : never;

type Equal<X, Y> = (<G>() => G extends X ? 1 : 2) extends (<G>() => G extends Y ? 1 : 2) ? true : false;
type Expect<V extends true> = V;
type tests = [
  Expect<Equal<RouteParams<"/shops/:shopId/products/:productId">, "shopId" | "productId">>,
  Expect<Equal<RouteParams<"/about">, never>>,
];

function fill<P extends string>(path: P, params: Record<RouteParams<P>, string | number>): string {
  return path.replace(/:(\w+)/g, (_, k: string) => String((params as Record<string, string | number>)[k]));
}
console.log(fill("/shops/:shopId/products/:productId", { shopId: "s1", productId: 42 }));

Output

/shops/s1/products/42