TypeScript Interview Questions

TypeScript questions check whether you can model real data safely and read an error message, rather than reaching for `any` when the compiler pushes back.

This topic has 34 curated TypeScript questions covering narrowing, unknown vs any, discriminated unions, generics and runtime validation. Below are 8 of them in full, with the same answers you get in the interactive tool.

Practise all 34 TypeScript questions →

Difficulty breakdown

  • Easy6
  • Medium14
  • Hard12
  • Expert2

Question formats

  • Multiple choice13
  • Concept5
  • Predict the output5
  • Write code3
  • Best practice2
  • Code review2

What these questions cover

  • types
  • narrowing
  • safety
  • generics
  • modelling
  • strictness
  • api-design
  • react
  • runtime
  • api

8 example TypeScript interview questions

EasyBest practice

What makes a component’s prop types good rather than merely present?

Good prop types make wrong usage impossible rather than merely annotated. Prefer a union of literals over a string (`variant: "primary" | "ghost"` rather than `variant: string`), so typos are caught and the options are discoverable. Prefer required props over optional-with-a-default when the component genuinely cannot work without them. Model mutually exclusive combinations as a union of prop shapes rather than several optional booleans, so illegal combinations do not type-check. Avoid `any` and avoid over-wide types like `object` or `Function`. And keep the prop surface small — a type with fifteen optional properties is usually telling you the component does too much.

The underlying idea is that a prop type is an API contract, not documentation — its job is to make the wrong call fail at the call site rather than to describe what is expected. The union-of-shapes point is the one that separates adequate from good: four optional booleans allow sixteen combinations, most of which the component cannot render, and no amount of runtime guarding makes them unrepresentable. Shrinking the prop surface is the other half; fifteen optional props is usually a sign that two components are sharing one implementation.

MediumWrite code

Given a union response type, write the handling so every case is covered and the compiler enforces it

type Res =
  | { status: 'ok'; data: string[] }
  | { status: 'empty' }
  | { status: 'error'; message: string };

Switch on the discriminant — `res.status` — so each branch narrows to exactly the member that has those fields, then add a `default` branch that assigns the value to a `never`-typed variable. Today that compiles, because every member is already handled and the value in `default` has type `never`. The moment a fourth status is added to the union, the value reaching `default` is no longer `never` and the assignment fails to compile, pointing at every switch that forgot the new case. No assertions are needed anywhere, because the discriminant does all the narrowing.

The exhaustiveness check is the part worth internalising: without it, adding a union member is a silent change that compiles everywhere and quietly falls through at runtime, and you find out from a bug report. With it, the compiler hands you the list of places to update. This is also why discriminated unions beat optional fields for modelling responses — `{ status, data?, message? }` forces every consumer to guess which fields are present, and no check can make that safe.

HardCode review

Review this API client’s types

export enum Status { Ok, Error }
export interface Result {
  status: Status;
  data?: unknown;
  error?: string;
}

The model permits states that cannot happen: `{ status: Ok }` with no data, or both `data` and `error` set, and every consumer must handle those impossible cases or risk a crash. Replace with a discriminated union — `{ status: "ok"; data: T } | { status: "error"; error: string }` — so narrowing on `status` gives exactly the right fields and the compiler enforces exhaustiveness. Additional points: the numeric enum means `Status` accepts any number, and serialises as 0/1 which is opaque in logs and fragile across versions; string literals are better. And `data: unknown` pushes an unchecked cast onto every caller — make `Result` generic instead.

Making illegal states unrepresentable is the single highest-leverage TypeScript idea, and this snippet is the canonical counter-example.

EasyConcept

What is narrowing, and which everyday checks perform it?

Narrowing is the compiler tracking a value’s type getting more specific inside a branch, so a `string | number` becomes just `string` after a check. The checks that do it are the ones you already write: `typeof x === "string"`, `Array.isArray(x)`, `x instanceof Date`, a truthiness check to remove `null | undefined`, `"key" in obj` for object shapes, and comparing a literal-typed discriminant like `if (res.status === "ok")`. Narrowing is why well-typed code rarely needs assertions — if you find yourself writing `as`, the usual fix is a check the compiler can follow. It is also why `unknown` is usable: the compiler forces you to narrow before use, which is exactly the check you should have written anyway.

The useful mental model is that narrowing is the compiler following the same reasoning you already do when you read the code — it is not a feature you invoke, it is what the checks you write already mean. That reframing is what makes `as` look suspicious: an assertion tells the compiler to stop reasoning, so it is a claim you are making instead of one it verified. The one common surprise is that narrowing does not survive a callback or an `await` on a mutable value, because the compiler cannot prove nothing reassigned it in between; copying to a `const` first is the usual fix.

MediumDebugging

The compiler is silent but this crashes at runtime. Where did type safety disappear?

const config = JSON.parse(raw);          // any
const port: number = config.server.port;  // no error
port.toFixed(2);                          // TypeError at runtime

`JSON.parse` returns `any`, and `any` propagates: every property access off it is also `any`, which is assignable to `number` without complaint. The compiler is not wrong — it was told to stop checking. Fix by typing the boundary as `unknown` and narrowing with a type predicate or a schema validator before use, so the shape is verified once at the edge rather than assumed everywhere after it.

The lesson is that `any` is not a local escape hatch; it silently disables checking for everything downstream of it, which is why `unknown` is the right type for external data.

EasyFollow-up

You typed the API response. Does that mean the data is guaranteed to match at runtime?

No. Types are erased at compile time, so an annotation is a claim about what you EXPECT, not a check on what arrived. If the API changes a field, the compiler stays happy and the failure appears later as a confusing runtime error somewhere downstream. To actually know, you have to validate at the boundary — a type predicate or a schema parser that checks the shape and produces the type as a result. Everything inside the boundary can then trust the type; everything crossing it cannot. The practical rule is that `as SomeType` on parsed JSON is the moment safety was assumed rather than established.

This is the misconception with the widest blast radius, because the failure is displaced: the bad data enters at the fetch and the error surfaces three components away, where nothing looks wrong. The boundary framing is what makes it tractable — validate once where data enters, and the rest of the codebase gets to trust its types honestly. It is also why `as` on parsed JSON is worth flagging in review: it is not a type annotation, it is an unchecked assertion that happens to compile.

MediumMultiple choice

Why do many codebases prefer a string-literal union over a TypeScript `enum`?

A union has zero runtime footprint and its values are plain strings, which serialise and compare naturally. A standard enum generates a real object, and numeric enums additionally allow any number to be assigned, which weakens the check. `const enum` avoids the emit but has its own build caveats.

HardPredict the output

Why does the first assignment error and the second not?

type P = { a: number };
const x: P = { a: 1, b: 2 };            // error
const tmp = { a: 1, b: 2 };
const y: P = tmp;                       // ok

Excess property checking applies only to OBJECT LITERALS assigned directly to a typed target. Assigning through a variable is a plain structural compatibility check, and `{ a: number; b: number }` is a valid subtype of `P`, so it passes.

The literal check is a deliberate usability feature — it catches typos in options objects — rather than a soundness rule, which is exactly why it disappears through a variable. Knowing this explains why a refactor that extracts an object into a variable can silently stop catching a typo.

The remaining 26 TypeScript questions — plus mock interviews, spaced revision and progress tracking — are in the free interview prep workspace.

Related interview topics