Skip to content

7. TypeScript Interview Questions

Grouped Basic → Intermediate → Advanced. TypeScript rounds are usually shorter than React rounds — depth matters less than being fluent in the common cases.


Basic


1. What is TypeScript and why use it?

A statically typed superset of JavaScript that compiles to plain JavaScript. Every valid JavaScript file is a valid TypeScript file.

The value is refactoring confidence and self-documenting interfaces between modules — rename a field and the compiler lists every affected file. "Fewer bugs" is the answer every candidate gives; this one is better.


2. Does TypeScript run in the browser or Node?

No. It erases completely at compile time.

ts
const add = (a: number, b: number): number => a + b;
// compiles to
const add = (a, b) => a + b;

Consequences: no runtime cost, no runtime type checks, and no protection from bad API data unless you validate it yourself


3. What are the basic types?

string, number, boolean, null, undefined, bigint, symbol, plus any, unknown, never, void, arrays, tuples, objects and enums.


4. Array vs tuple?

An array is variable-length with one element type: number[]. A tuple is fixed length with a type per position: [string, number].

useState returns a tuple — that's why destructuring [value, setValue] gives you two different types.


5. any vs unknown?

Both accept any value. The difference is what you can do with them.

ts
let a: any = getData();
a.foo.bar();          // compiles, may crash

let u: unknown = getData();
// u.foo;             // Error
if (typeof u === "string") u.toUpperCase();   // must narrow first

any disables type checking and spreads through everything it touches. unknown is the safe version — use it at every boundary where external data enters (JSON.parse, API responses, catch).


6. What is never?

A type with no possible values — for functions that never return (they throw or loop forever), and for impossible types like string & number.

Its practical use is exhaustiveness checking:

ts
default: {
  const _exhaustive: never = status;   // compile error if a case is unhandled
}

Add a new union member and the file stops compiling until you handle it.


7. type vs interface?

Both describe object shapes. Differences:

interfacetype
Unions, tuples, primitivesNoYes
Declaration mergingYesNo
Extensionextends&
Mapped / computed typesNoYes

Working answer: interface for object shapes and public API surfaces someone might augment; type for unions, tuples and anything computed. Most teams pick one and stay consistent.

Where it genuinely matters: extending Express's Request with req.user requires declaration merging, which only interface can do.


8. Union vs intersection?

| is "one of these", & is "all of these at once".

The trap: on a union you can only access members present on every member until you narrow. A | B does not give you A's properties and B's properties — it gives you their intersection of accessible members.


9. What are literal types and as const?

ts
type Direction = "up" | "down";   // literal union — autocomplete + typo safety

as const freezes inferred types to their literal values:

ts
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number];   // "admin" | "editor" | "viewer"

One source of truth for both the runtime array and the type.


10. Optional ? vs | undefined?

ts
interface A { x?: number }              // may be absent
interface B { x: number | undefined }   // must be present, may be undefined

const a: A = {};       // OK
const b: B = {};       // Error

Under exactOptionalPropertyTypes they become fully distinct.


11. What does readonly do?

Prevents reassignment of a property. It is shallowreadonly options: { debug: boolean } blocks config.options = {} but allows config.options.debug = true.


12. Why do most teams avoid enums?

  • Numeric enums are not type-safe — a value outside the members can be assigned
  • Enums emit real JavaScript, so they are not fully erasable
  • const enum is banned under isolatedModules, which most bundlers require

The modern alternative:

ts
const Status = { Active: "ACTIVE", Inactive: "INACTIVE" } as const;
type Status = typeof Status[keyof typeof Status];

13. What is strict mode?

A group of flags: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables.

strictNullChecks is where most of the real value is — it eliminates "cannot read property of undefined". Without strict, TypeScript gives you autocomplete and very little safety.

Also worth adding: noUncheckedIndexedAccess, which makes arr[0] return T | undefined — the truth.


Intermediate


14. What is type narrowing? Name the ways.

Narrowing is TypeScript deducing a more specific type inside a block.

typeof, truthiness, equality (===, != null), the in operator, instanceof, discriminated unions, custom type guards (x is T), and assertion functions (asserts x is T).


15. What is a discriminated union and why does it matter?

A union of object types sharing a literal field that tells them apart.

ts
type State =
  | { status: "loading" }
  | { status: "error"; message: string }
  | { status: "success"; data: User[] };

Switch on status and TypeScript narrows automatically — you cannot read state.data in the error branch.

Why it beats optional fields: with { loading?, data?, error? } you can represent loading: true and error: "..." simultaneously — an impossible state. Discriminated unions make illegal states unrepresentable.


16. What is a type guard?

A function whose return type is arg is Type:

ts
function isAdmin(u: User | Guest): u is Admin {
  return u.role === "admin";
}

The danger: TypeScript does not verify the body matches the claim. return true compiles and lies. That is precisely why Zod exists.


17. What is an assertion function?

ts
function assertDefined<T>(v: T | null | undefined): asserts v is T {
  if (v == null) throw new Error("Expected a value");
}

Narrows for the rest of the scope, not just inside a block. A safer replacement for the ! non-null assertion, because it actually checks.


18. What are generics and when do you need one?

A type parameter that preserves the relationship between input and output types.

ts
function first<T>(arr: T[]): T | undefined { return arr[0]; }

The rule: a type parameter must appear in at least two positions to be doing work. function log<T>(v: T): void is pointless — use unknown.


19. Write a type-safe property getter.

The most-asked "write a generic" question:

ts
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

getProperty(user, "name");   // string
// getProperty(user, "age"); // Error — not a key of User

The return type is T[K], so the exact property type flows through. keyof plus indexed access is the pattern.


20. Which utility types do you use most?

  • Partial<T> — PATCH bodies, optional config
  • Omit<T, K> / Pick<T, K> — API request and response shapes derived from one entity
  • Record<K, V> — exhaustive lookup tables keyed by a union
  • ReturnType + Awaited — deriving types from functions so they never drift

The point is a single source of truth: one User type, every DTO derived from it.


21. Pick or Omit for an API response type?

A trade-off worth naming. Omit<User, "passwordHash"> is convenient — but if someone later adds twoFactorSecret to User, it is automatically included in the response. That's a leak.

Pick is fail-safe: new fields are excluded until you explicitly add them.

Answer: Pick (allowlist) for anything crossing a trust boundary; Omit for internal convenience.

Also note Omit does not error on a key that doesn't exist; Pick does.


22. What is keyof and indexed access?

ts
type UserKey = keyof User;        // "id" | "name" | "email"
type NameType = User["name"];     // string
type Values = User[keyof User];   // number | string

Together they let you write generics that operate over an object's real keys.


23. What is satisfies and why is it useful?

Validates a value against a type without widening the inferred type.

ts
const config = { mode: "dark", retries: 3 } satisfies Config;
config.mode;   // "dark" — still literal

With : Config you'd get string. satisfies gives you the check and the narrow type. TypeScript 4.9+.


24. What is declaration merging?

Two declarations with the same name combine. Only interfaces and namespaces do this.

The classic use is augmenting a third-party type:

ts
declare global {
  namespace Express {
    interface Request { user?: User }
  }
}
export {};

This is the reason interface still matters in a codebase that otherwise prefers type.


25. void vs undefined as a return type?

ts
type Fn = () => void;
const f: Fn = () => 42;   // allowed

void means "the caller must not rely on the return value", not "must return nothing". That's why arr.forEach(x => arr2.push(x)) compiles even though push returns a number.


26. What are type assertions and when are they dangerous?

value as Type tells the compiler "trust me". It performs no conversion and no check.

ts
const x = "hello" as unknown as number;
x.toFixed(2);   // crashes at runtime

The ! non-null assertion is the same risk in miniature. Prefer an explicit check or an assertion function.


27. How do you type a React component's props?

tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "ghost";
}

function Button({ variant = "primary", ...rest }: ButtonProps) { … }

Extend the native element attributes so consumers get every standard prop for free.

Don't use React.FC — it used to implicitly add children (removed in React 18 types), makes generic components awkward, and adds nothing.


28. How do you type useState, useRef and useReducer?

tsx
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);   // empty array needs the annotation

const inputRef = useRef<HTMLInputElement>(null);    // DOM ref, read-only current
const timerRef = useRef<number | null>(null);       // mutable value

const [state, dispatch] = useReducer(reducer, initialState);

For useReducer, type Action as a discriminated union so each action's payload requirements are exact.

useState([]) without an annotation infers never[] — a very common error.


29. How do you type a custom hook?

Return as const so the tuple positions keep their types:

tsx
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((p) => !p), []);
  return [on, toggle] as const;
}

Without it, the return widens to (boolean | (() => void))[] and destructuring loses everything.


30. Why does <T>(x: T) => x fail in a .tsx file?

It is parsed as a JSX tag. Fix with a trailing comma — <T,>(x: T) => x — or <T extends unknown>.


Advanced


31. TypeScript types are compile-time only. How do you handle API data?

The most important practical question on this page.

ts
const user: User = await res.json();   // a lie — res.json() returns any

The annotation checks nothing. If the API changes, you crash three components deep with a confusing error.

The fix is runtime validation with the type derived from the schema:

ts
const UserSchema = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof UserSchema>;

const user = UserSchema.parse(await res.json());   // throws at the boundary

One-liner: "TypeScript is compile-time only, so I validate every external boundary at runtime with Zod and derive the TypeScript type from the schema."

Same schema handles API responses, form input and environment variables.


32. What are conditional types and infer?

ts
type IsString<T> = T extends string ? true : false;
type Unwrap<T> = T extends Promise<infer U> ? U : T;

infer captures a type in a matching position. It is how ReturnType is implemented:

ts
type ReturnType<T extends (...a: any) => any> =
  T extends (...a: any) => infer R ? R : any;

For application work, being able to read these matters more than writing them.


33. What are mapped types?

Transform every property of a type:

ts
type Optional<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };   // - removes a modifier
type Required<T> = { [K in keyof T]-?: T[K] };

With key remapping:

ts
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };

The genuinely useful one in real code:

ts
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type NewUser = PartialBy<User, "id" | "createdAt">;   // the create DTO

34. What is a distributive conditional type?

Conditional types distribute over unions automatically:

ts
type NonNull<T> = T extends null | undefined ? never : T;
type A = NonNull<string | null>;   // string

Prevent distribution by wrapping in a tuple: [T] extends [U] ? … : ….


35. How do you extend Express's Request with req.user?

Declaration merging:

ts
// types/express.d.ts
declare global {
  namespace Express {
    interface Request { user?: User }
  }
}
export {};   // required — makes the file a module

The alternative is a dedicated AuthedRequest extends Request interface with user required, which is cleaner typing but needs a cast at route registration.


36. Why is catch (error) typed unknown?

Because JavaScript lets you throw anything — throw "oops" is legal. Under useUnknownInCatchVariables (part of strict), TypeScript is being honest.

ts
catch (error) {
  if (error instanceof Error) console.error(error.message);
  else console.error("Unknown error", error);
}

37. Why does instanceof fail on a custom Error subclass?

When targeting ES5, extending a built-in breaks the prototype chain. Fix:

ts
export class AppError extends Error {
  constructor(message: string, public statusCode = 500) {
    super(message);
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

A favourite Node + TypeScript question.


38. How do you type environment variables?

Validate once at startup and export the parsed object:

ts
export const env = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  PORT: z.coerce.number().default(3000),
}).parse(process.env);

process.env.X is string | undefined — using it raw means every access needs a guard, and a missing secret fails at 3am instead of at boot. Fail fast at startup.


39. Why do ESM imports need a .js extension in TypeScript?

With "module": "NodeNext" and "type": "module":

ts
import { getUser } from "./services/user.js";   // .js, even from a .ts file

TypeScript is describing the path of the emitted file, not the source. Confuses everyone once.


40. Does tsx / esbuild / SWC type-check your code?

No. They strip types without checking them. Same for Node's native TypeScript support.

Keep tsc --noEmit as a separate CI step. Type checking is a build-time gate, not a runtime feature — without an explicit step, broken types ship happily.


41. Structural vs nominal typing — which does TypeScript use?

Structural. Compatibility is decided by shape, not by declared name.

ts
interface Point { x: number; y: number }
class Vec { constructor(public x: number, public y: number) {} }

const p: Point = new Vec(1, 2);   // OK — shapes match

The problem this causes: type UserId = string and type PostId = string are interchangeable, so you can pass one where the other is expected.

The fix — branded types:

ts
type UserId = string & { readonly __brand: "UserId" };

function asUserId(id: string): UserId { return id as UserId; }

function getUser(id: UserId) { … }
// getUser("some-random-string");   // Error

Nominal typing simulated on top of a structural system. Worth knowing for the ID-mix-up class of bug.


42. What is excess property checking?

Object literals get an extra check that variables don't:

ts
interface User { name: string }

const a: User = { name: "Dipak", age: 25 };   // Error — age not in User

const obj = { name: "Dipak", age: 25 };
const b: User = obj;                           // OK — no literal, no check

It exists to catch typos in inline object arguments. It surprises people because assigning the same object through a variable passes.


43. How do you handle a third-party library with no types?

In order of preference:

  1. npm i -D @types/library-name — check DefinitelyTyped first
  2. Write a local declaration file:
ts
// types/some-lib.d.ts
declare module "some-lib" {
  export function doThing(input: string): Promise<number>;
}
  1. As a last resort, declare module "some-lib"; — types it as any, which at least keeps the build green while you decide

Never reach for // @ts-ignore as the first move. // @ts-expect-error is strictly better when you must suppress — it errors if the line stops erroring, so it cleans itself up.


44. How would you migrate a JavaScript codebase to TypeScript?

  1. Add tsconfig.json with allowJs: true and strict: false — nothing breaks yet
  2. Add checkJs: false; type-check nothing initially
  3. Rename leaf files (utilities, constants) to .ts first — they have the fewest dependents
  4. Work up the dependency graph toward components and entry points
  5. Turn on flags one at a time: noImplicitAny, then strictNullChecks, then full strict
  6. Ban new any with an ESLint rule; let existing ones stay until touched
  7. Add tsc --noEmit to CI once the error count reaches zero

The key point: incremental and mixed. A big-bang rewrite stalls, and allowJs exists precisely so you don't need one.


45. What would you flag in a TypeScript code review?

  • any anywhere, especially at an API boundary — should be unknown + validation
  • as assertions hiding a real type mismatch
  • ! non-null assertions without a nearby check
  • API responses typed by annotation instead of validated at runtime
  • strict off, or // @ts-ignore instead of // @ts-expect-error
  • Types duplicated instead of derived (Omit, Pick, z.infer, ReturnType)
  • Optional-field state objects where a discriminated union belongs
  • Generics with a type parameter used in only one position
  • Enums where an as const object would do
  • Services importing Express types, coupling business logic to HTTP

© 2025 DDocs · Dipak's Documentation Guide