Skip to content
TypeScript ts interview 5 min read

Scenario-Based Questions

Scenario questions describe a realistic problem and ask how you would model or type it. They reward judgement, not memorization: there is often more than one defensible answer, and the interviewer wants to hear your reasoning about trade-offs. The scenarios below cover modeling domain data, designing flexible function signatures, typing external input safely, and structuring shared types. Each comes with a model answer you can adapt to your own words.

Modeling scenarios

How would you type an API response whose shape you do not fully control?

Treat the boundary as untrusted. Type the raw value as unknown, then validate and narrow it into a known type before letting it flow into your app.

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  const data: unknown = await res.json();
  if (!isUser(data)) throw new Error("Invalid user payload");
  return data; // narrowed to User
}

function isUser(v: unknown): v is User {
  return typeof v === "object" && v !== null && "id" in v && "name" in v;
}

For real projects, mention a runtime validator like Zod that derives the static type from a schema, eliminating drift between the type and the check.

You need a config object where some fields are required and others optional with defaults. How do you type it?

Model the public (caller-facing) type with optional fields, and a resolved internal type where defaults are filled in. Required or an intersection makes the resolved type explicit.

interface UserConfig {
  host: string;          // required
  port?: number;         // optional
  retries?: number;
}
type ResolvedConfig = Required<UserConfig>;

function init(cfg: UserConfig): ResolvedConfig {
  return { port: 8080, retries: 3, ...cfg };
}

This keeps the API ergonomic for callers while giving internal code non-optional fields.

How do you model an entity that can be in one of several states, each carrying different data?

A discriminated union. Each state is an object with a shared literal tag and only the fields valid for that state, so impossible combinations cannot be constructed.

type Request =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; error: Error };

function render(r: Request) {
  switch (r.status) {
    case "success": return r.data;
    case "error": return r.error.message;
    default: return "...";
  }
}

This prevents bugs like reading data while still loading.

You have a function that should return a different type based on a boolean flag. How do you type it?

Use function overloads (or a conditional return type) so the return narrows by the argument, rather than returning a union the caller must re-narrow.

function parse(input: string, asJson: true): object;
function parse(input: string, asJson?: false): string;
function parse(input: string, asJson?: boolean): object | string {
  return asJson ? JSON.parse(input) : input;
}

const a = parse("{}", true); // object
const b = parse("hi");        // string

Overloads give callers precise return types without casts.

Design scenarios

How would you write a type-safe pick utility?

Constrain the keys to keyof T with a generic, and build the return type with a mapped type over the selected keys.

function pick<T, 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;
}

const user = { id: 1, name: "Ada", age: 36 };
const slim = pick(user, ["id", "name"]); // { id: number; name: string }
// pick(user, ["missing"]); // ❌ not assignable to keyof T

The K extends keyof T constraint rejects keys the object does not have.

You want to make all properties of a type deeply readonly. How?

Write a recursive mapped type that reapplies itself to nested object properties.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

type Config = DeepReadonly<{ db: { url: string }; debug: boolean }>;
// config.db.url is readonly, and config.db itself is readonly

Note the built-in Readonly<T> is shallow; recursion is what makes it deep.

How would you enforce that two props are provided together or not at all?

Use a union of “both present” and “both absent” object types so partial combinations are rejected.

type WithRange =
  | { min: number; max: number }
  | { min?: undefined; max?: undefined };

function slider(opts: WithRange) {}
slider({ min: 0, max: 10 }); // ✅
slider({});                  // ✅
// slider({ min: 0 });       // ❌ max required when min is present

How do you share types between a frontend and backend without duplication?

Define the contract types in a shared package (or shared folder) and import them on both sides, ideally deriving them from a single source of truth such as a validation schema or an ORM model.

// shared/types.ts
export interface User { id: string; name: string; email: string }

// frontend and backend both:
import type { User } from "@app/shared/types";

Mention import type so the shared module is erased from runtime bundles, and a schema-first tool to keep validation and types in lockstep.

In scenario questions, always state your assumptions and trade-offs out loud. Saying “I’d use a discriminated union because it makes invalid states unrepresentable” scores higher than silently writing correct code.

Best Practices

  • Validate external data at the boundary; type it unknown until proven safe.
  • Reach for discriminated unions to make invalid states unrepresentable.
  • Use generic constraints (K extends keyof T) to keep utilities type-safe.
  • Prefer overloads or conditional return types over unions the caller must narrow.
  • Share contract types from one source of truth; use import type to keep them runtime-free.
  • Distinguish shallow built-ins (Readonly) from recursive variants you must write yourself.
  • Talk through assumptions and trade-offs — the reasoning is what is being assessed.
Last updated June 29, 2026
Was this helpful?