{}The Interview
Handbook

Tracks / TypeScript

TypeScript types, generics & config

mid 10 questions · 6 min read typescriptgenericstypes

Questions in this set 10
  1. 01What does "structural typing" mean, and how does it differ from nominal typing?
  2. 02interface vs type — which do you use?
  3. 03any vs unknown vs never.
  4. 04Explain narrowing and type guards.
  5. 05Write a generic function with constraints.
  6. 06Which utility types do you actually use?
  7. 07Which tsconfig flags matter most?
  8. 08What is the difference between compile-time and runtime in TypeScript?
  9. 09How do you type an async API client well?
  10. 10What are declaration files and module augmentation for?
01

What does "structural typing" mean, and how does it differ from nominal typing?

TypeScript compares types by shape, not by name. Anything with the required members is assignable, regardless of declared inheritance.

ts
interface Point { x: number; y: number }
class Vec { constructor(public x: number, public y: number) {} }
const p: Point = new Vec(1, 2);           // fine — same shape

The consequence people trip on: type UserId = string and type OrderId = string are the same type, so you can pass one where the other is expected. Fix with branded types:

ts
type UserId = string & { readonly __brand: "UserId" };
const asUserId = (s: string) => s as UserId;
function getUser(id: UserId) {}
getUser("oops");            // Error — a plain string is not a UserId
02

interface vs type — which do you use?

interface supports declaration merging and extends, and produces slightly nicer error messages for object shapes. type can express unions, intersections, tuples, mapped and conditional types — everything interface cannot.

Practical rule: interface for object shapes you may extend or that are part of a public API (a library's props can be augmented by consumers); type for everything else. Do not spend interview time on this — the honest answer is "they overlap heavily; be consistent within a codebase."

03

any vs unknown vs never.

  • any turns the checker off for that value — it propagates and silently disables safety downstream. Treat it as a bug.
  • unknown is the safe top type: you can hold anything but must narrow before using it. The correct type for JSON.parse, catch clauses and untrusted input.
  • never is the bottom type: no value inhabits it. It is the return type of a function that always throws, the type of an impossible branch, and the tool for exhaustiveness checking:
ts
function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.side ** 2;
    default:
      const _exhaustive: never = s;    // compile error if a new Shape variant is added
      throw new Error(`unhandled: ${JSON.stringify(s)}`);
  }
}

That pattern is the single most valuable thing discriminated unions buy you, and it is worth showing unprompted.

04

Explain narrowing and type guards.

The compiler narrows a union based on control flow: typeof, instanceof, in, truthiness, equality, and discriminant properties. When that is not enough, write a guard:

ts
function isUser(v: unknown): v is User {          // user-defined type predicate
  return typeof v === "object" && v !== null && "email" in v;
}

function assertDefined<T>(v: T): asserts v is NonNullable<T> {   // assertion function
  if (v == null) throw new Error("expected a value");
}

The caveat to raise: a type predicate is an unchecked promise — TypeScript trusts your boolean. If the runtime check is wrong, you have lied to the compiler. For data crossing a real boundary (an API response), use a runtime validator (Zod, Valibot) and derive the type with z.infer, so there is exactly one source of truth.

05

Write a generic function with constraints.

ts
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(i => i[key]);
}
const names = pluck(users, "name");    // string[] — inferred, no annotation needed

function merge<A extends object, B extends object>(a: A, b: B): A & B {
  return { ...a, ...b };
}

K extends keyof T is the workhorse constraint. Also know extends in conditional position, which is a different thing:

ts
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;   // `infer` extracts a type
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
06

Which utility types do you actually use?

Partial<T> · Required<T> · Readonly<T> · Pick<T, K> · Omit<T, K> · Record<K, V> · Exclude<U, X> / Extract<U, X> · NonNullable<T> · ReturnType<F> / Parameters<F> · Awaited<T>.

And be able to implement a couple from scratch, because that is the actual question:

ts
type MyPartial<T>     = { [K in keyof T]?: T[K] };
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
type DeepReadonly<T>  = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] };
07

Which tsconfig flags matter most?

jsonc
{
  "compilerOptions": {
    "strict": true,                         // the one that matters; enables the family below
    "noUncheckedIndexedAccess": true,       // arr[i] is T | undefined — catches real bugs
    "exactOptionalPropertyTypes": true,     // {a?: string} ≠ {a: undefined}
    "noImplicitOverride": true,
    "isolatedModules": true,                // required by esbuild/swc/Babel transpilers
    "verbatimModuleSyntax": true,
    "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler"
  }
}

strict bundles strictNullChecks (the big one — null/undefined stop being assignable to everything), noImplicitAny, strictFunctionTypes and more. Without strictNullChecks TypeScript catches a small fraction of the bugs it could. If asked "how do you migrate a JS codebase", the answer is: allowJs + checkJs on a few files, strict: false initially, then turn flags on one at a time, file by file, with // @ts-expect-error (never @ts-ignoreexpect-error fails when the error goes away, so it self-cleans).

08

What is the difference between compile-time and runtime in TypeScript?

Types are erased. There is no type information at runtime, so you cannot switch on a generic parameter, instanceof a type, or validate an API response by its declared type. enum and class and decorators emit real code; interface, type and as emit nothing.

The consequence to state: as is not a conversion, it is an assertion — telling the compiler to stop arguing. const u = data as User on an untrusted payload gives you the false confidence of a type with none of the checking. Validate at the boundary; trust types only inside it.

09

How do you type an async API client well?

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

async function getJSON<T>(url: string, schema: z.ZodType<T>): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { ok: false, error: new HttpError(res.status) };
    const parsed = schema.safeParse(await res.json());
    return parsed.success
      ? { ok: true, value: parsed.data }
      : { ok: false, error: new Error("schema mismatch") };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
  }
}

Points here: a discriminated Result makes the error path impossible to forget (unlike a thrown exception the caller may not know about); the schema provides both runtime validation and the static type; and catch (e) gives you unknown, which must be narrowed — a strict-mode detail many candidates get wrong.

10

What are declaration files and module augmentation for?

.d.ts files describe the types of JavaScript that has none — either shipped with a package (types in package.json) or from DefinitelyTyped (@types/*). Module augmentation extends someone else's types from your code:

ts
declare module "express" {
  interface Request { user?: { id: string; roles: string[] } }   // typed req.user
}
declare global {
  interface Window { __APP_CONFIG__: AppConfig }
}

This is how you add a property to a third-party interface without forking it — and the correct answer to "how do you type req.user after your auth middleware sets it?"