Interview Q&A · 100 questions · 20 answered publicly

TypeScript Developer Interview Questions

Ordered by how often they're asked. Sign in free to see every answer, rate your confidence and run timed mock interviews.

  1. Most asked What is TypeScript and what problems does it solve?

    Summary

    TypeScript is a statically typed superset of JavaScript that adds a type system and compiles (by erasing types) to plain JavaScript. It catches type errors at compile time, improves editor tooling and documents intent in large codebases.

    Longer Answer

    Any valid JavaScript is (almost always) valid TypeScript. The type checker (tsc) analyses code without running it; types are erased in the output, so there's no runtime type checking. Benefits include safer refactors, autocompletion, self-documenting APIs and catching undefined is not a function bugs before production. Tools like esbuild, SWC and Node's type stripping can remove types without checking them, so teams run tsc --noEmit separately in CI.

    Key Points

    • Superset of JavaScript
    • Static checking, types erased at compile time
    • No runtime type validation
    • Better tooling and refactoring

    Permalink

  2. Most asked Explain `any`, `unknown` and `never`.

    Summary

    any disables type checking for a value; unknown is the type-safe top type: anything can be assigned to it, but you must narrow it before use; never is the bottom type for values that never occur (functions that throw, exhaustive checks).

    Longer Answer

    Prefer unknown for untrusted input (JSON, catch variables with useUnknownInCatchVariables) and narrow with typeof, instanceof or schema validation. any spreads silently through code and should be rare (lint with no-explicit-any). never is useful in exhaustive switch statements: assigning the remaining value to never makes the compiler error when a new union member is added.

    Code Example

    function parse(json: string): unknown {
      return JSON.parse(json);
    }
    
    const data = parse('{"name":"Ana"}');
    // data.name;  // Error: 'data' is of type 'unknown'
    if (typeof data === "object" && data !== null && "name" in data) {
      console.log((data as { name: string }).name);
    }
    
    function assertNever(x: never): never {
      throw new Error(`Unexpected: ${JSON.stringify(x)}`);
    }
    

    Key Points

    • any: opt out of checking (contagious)
    • unknown: must narrow before use
    • never: impossible values, exhaustiveness
    • Prefer unknown for external data

    Comparison / Tradeoffs

    Type Assign anything to it? Use without checks?
    any Yes Yes (unsafe)
    unknown Yes No, narrow first
    never No N/A

    Permalink

  3. Most asked What are the differences between `interface` and `type` aliases?

    Summary

    Both can describe object shapes and be extended. Interfaces support declaration merging and extends, which reads well for object/class contracts; type aliases can name any type, including unions, tuples, primitives, mapped and conditional types.

    Longer Answer

    Interfaces with the same name in the same scope merge, which is used to augment library types (e.g., adding fields to Window or Express's Request). Type aliases can't be reopened. For object shapes, either is fine; many teams use interface for public object contracts and type for unions and computed types. Interface extends errors on conflicting properties, while intersections (&) may silently produce never properties.

    Code Example

    interface User { id: string; name: string }
    interface User { email: string }        // merged: { id, name, email }
    
    type Status = "active" | "banned";      // only possible with type alias
    type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date };
    

    Key Points

    • Interfaces: declaration merging, extends
    • Types: unions, tuples, mapped/conditional types
    • Both work for object shapes
    • Use merging to augment library types

    Permalink

  4. Most asked What is structural typing in TypeScript?

    Summary

    TypeScript checks compatibility by shape (structure), not by declared names: a value is assignable to a type if it has all the required members with compatible types, regardless of which class or interface created it.

    Longer Answer

    This differs from nominal typing in Java or C#. Two unrelated interfaces with the same members are interchangeable. Excess property checks only apply to fresh object literals. When you need nominal-like behavior (e.g., not mixing UserId and OrderId strings), use branded types.

    Code Example

    interface Point { x: number; y: number }
    class Vector { constructor(public x: number, public y: number) {} }
    
    const p: Point = new Vector(1, 2); // OK: same shape
    
    type UserId = string & { readonly __brand: "UserId" };
    type OrderId = string & { readonly __brand: "OrderId" };
    declare function getUser(id: UserId): void;
    // getUser("abc" as OrderId); // Error: brands differ
    

    Key Points

    • Compatibility by shape
    • Unrelated types with same shape are interchangeable
    • Excess property checks on object literals
    • Brands simulate nominal typing

    Permalink

  5. Most asked What is type narrowing and what techniques does TypeScript support?

    Summary

    Narrowing refines a broad type to a more specific one within a code branch, based on control-flow analysis of checks like typeof, instanceof, in, equality, truthiness, discriminant properties, and user-defined type guards (x is T) or assertion functions (asserts x is T).

    Longer Answer

    The compiler tracks types through if/switch/early returns. Since TypeScript 5.5, the compiler can infer type predicates from simple functions like x => x !== null, so array.filter(...) narrows correctly. Assertion functions throw if the condition fails and narrow afterwards.

    Code Example

    type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number };
    
    function area(s: Shape): number {
      switch (s.kind) {
        case "circle": return Math.PI * s.r ** 2;   // s narrowed to circle
        case "rect":   return s.w * s.h;
      }
    }
    
    function isString(x: unknown): x is string {
      return typeof x === "string";
    }
    
    const ids = [1, null, 3].filter((x) => x !== null); // number[] (TS 5.5+)
    

    Key Points

    • typeof, instanceof, in, equality
    • Discriminated unions with switch
    • Custom guards: (x): x is T
    • Assertion functions: asserts x is T
    • TS 5.5 infers type predicates

    Permalink

  6. Most asked What are discriminated unions and why are they powerful?

    Summary

    A discriminated (tagged) union is a union of object types sharing a literal property (the discriminant, e.g., type or status). Checking that property narrows to the exact variant, and exhaustiveness checks ensure every case is handled.

    Longer Answer

    They model state machines and API results precisely, making invalid states unrepresentable (e.g., { status: 'success'; data } vs { status: 'error'; error } instead of optional data? and error?). Combined with never-based exhaustiveness checks, adding a variant forces updates everywhere.

    Code Example

    type Result<T> =
      | { status: "loading" }
      | { status: "success"; data: T }
      | { status: "error"; error: Error };
    
    function render(r: Result<string[]>) {
      switch (r.status) {
        case "loading": return "Loading…";
        case "success": return r.data.join(", ");
        case "error":   return r.error.message;
        default: {
          const _exhaustive: never = r;
          return _exhaustive;
        }
      }
    }
    

    Key Points

    • Shared literal tag field
    • Narrowing per variant
    • Exhaustiveness via never
    • Make invalid states unrepresentable

    Permalink

  7. Most asked What are generics and how do constraints work?

    Summary

    Generics are type parameters (<T>) that let functions, classes and types work over many types while preserving type relationships. Constraints (<T extends Something>) restrict what T can be so you can safely use its members.

    Longer Answer

    Example: function first<T>(arr: T[]): T | undefined returns the same element type it receives. keyof constraints relate parameters: function get<T, K extends keyof T>(obj: T, key: K): T[K]. Defaults (<T = string>) and inference from arguments make generic APIs ergonomic. Avoid unnecessary generics: if T is used only once, a plain type is clearer.

    Code Example

    function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
    const user = { id: 1, name: "Ana" };
    const n = getProp(user, "name");   // string
    // getProp(user, "age");           // Error: '"age"' not in keyof user
    
    function longest<T extends { length: number }>(a: T, b: T): T {
      return a.length >= b.length ? a : b;
    }
    

    Key Points

    • Type parameters preserve relationships
    • extends constraints
    • K extends keyof T → T[K]
    • Inference from arguments
    • Don't over-genericize

    Permalink

  8. Most asked Which built-in utility types do you use most, and what do they do?

    Summary

    Common ones: 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 NoInfer<T> (5.4).

    Longer Answer

    They're built with mapped and conditional types. Example uses: Partial<User> for PATCH payloads, Omit<User, 'password'> for public DTOs, Record<Status, string> for exhaustive lookup tables, ReturnType<typeof createStore> to derive types from implementations, and Awaited<ReturnType<typeof fetchUser>> for async results.

    Code Example

    interface User { id: string; name: string; email: string; password: string }
    
    type PublicUser = Omit<User, "password">;
    type UserPatch = Partial<Pick<User, "name" | "email">>;
    type StatusLabels = Record<"active" | "banned", string>;
    
    async function fetchUser(id: string): Promise<PublicUser> { /* ... */ return null!; }
    type Fetched = Awaited<ReturnType<typeof fetchUser>>; // PublicUser
    

    Key Points

    • Partial/Required/Readonly modify properties
    • Pick/Omit select keys
    • Record for maps with known keys
    • ReturnType/Parameters/Awaited derive types

    Permalink

  9. Most asked What are mapped types? Show how `Partial` or `Readonly` could be implemented.

    Summary

    Mapped types create new types by iterating over keys: { [K in keyof T]: ... }. Modifiers ?, readonly, -? and -readonly add or remove optionality and readonly-ness, and as clauses remap or filter keys.

    Longer Answer

    Key remapping with template literal types enables things like generating getter names. Filtering keys uses as with a conditional resolving to never.

    Code Example

    type MyPartial<T> = { [K in keyof T]?: T[K] };
    type Mutable<T> = { -readonly [K in keyof T]: T[K] };
    
    type Getters<T> = {
      [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
    };
    type UserGetters = Getters<{ name: string; age: number }>;
    // { getName: () => string; getAge: () => number }
    
    type OnlyStrings<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };
    

    Key Points

    • [K in keyof T] iteration
    • ?/readonly modifiers and - removal
    • as for key remapping/filtering

    Permalink

  10. Most asked Explain conditional types and the `infer` keyword.

    Summary

    Conditional types (T extends U ? X : Y) choose a type based on assignability. infer declares a type variable inside the extends clause to capture part of a type, e.g. a function's return type or a promise's resolved value.

    Longer Answer

    When T is a naked type parameter, conditional types distribute over unions: ToArray<string | number> becomes string[] | number[]. Wrap in brackets ([T] extends [U]) to prevent distribution. Many built-in utilities (ReturnType, Awaited, Exclude) are conditional types.

    Code Example

    type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
    type ElementType<T> = T extends readonly (infer E)[] ? E : T;
    type UnwrapPromise<T> = T extends Promise<infer V> ? UnwrapPromise<V> : T;
    
    type A = MyReturnType<() => number>;    // number
    type B = ElementType<string[]>;          // string
    type C = UnwrapPromise<Promise<Promise<boolean>>>; // boolean
    

    Key Points

    • T extends U ? X : Y
    • infer R captures sub-types
    • Distributive over naked unions
    • [T] extends [U] disables distribution

    Permalink

  11. Most asked What does the `satisfies` operator do and how does it differ from a type annotation?

    Summary

    satisfies (TypeScript 4.9) checks that an expression conforms to a type without changing the expression's inferred type, so you get validation and keep precise literal types. An annotation (const x: T = ...) widens the variable to T.

    Longer Answer

    Typical use: configuration objects where you want to ensure all keys are valid and values have the right shape, but still want autocomplete on the specific keys and literal values.

    Code Example

    type Route = { path: string; auth: boolean };
    
    const routes = {
      home: { path: "/", auth: false },
      admin: { path: "/admin", auth: true },
    } satisfies Record<string, Route>;
    
    routes.admin.path;      // OK, and key "admin" is known
    // routes.missing;      // Error: property doesn't exist
    
    const annotated: Record<string, Route> = routes;
    annotated.anything;     // no error: keys widened to string
    

    Key Points

    • Validates without widening
    • Keeps literal/specific inferred types
    • Great for config and lookup objects
    • Annotations widen to the declared type

    Permalink

  12. Most asked What does `as const` do?

    Summary

    as const makes TypeScript infer the narrowest possible type: literal types instead of string/number, readonly properties, and readonly tuples instead of arrays.

    Longer Answer

    It's commonly used to derive union types from runtime arrays or objects, giving a single source of truth: const ROLES = ['admin', 'user'] as const; type Role = typeof ROLES[number];. Combine with satisfies for validated, literal configs.

    Code Example

    const ROLES = ["admin", "editor", "viewer"] as const;
    type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
    
    const config = { retries: 3, mode: "strict" } as const;
    // config.mode = "loose"; // Error: readonly
    

    Key Points

    • Literal types instead of widened ones
    • Deep readonly
    • Readonly tuples
    • Derive unions from runtime values

    Permalink

  13. Most asked Should you use enums or union types in TypeScript?

    Summary

    Many teams prefer string literal unions (optionally derived from an as const object) over enums: unions are erased at compile time, work with structural typing and plain strings, and are supported by type-stripping tools. Enums generate runtime objects and have quirks (numeric enums accept any number in older versions; const enum breaks with isolated modules).

    Longer Answer

    Enums are still fine for some codebases, especially string enums with explicit values. Node's built-in type stripping and erasableSyntaxOnly (TS 5.8) reject enums because they aren't erasable syntax, which pushes modern code toward unions or as const objects.

    Code Example

    // Preferred alternative to enum
    const Status = { Active: "active", Banned: "banned" } as const;
    type Status = (typeof Status)[keyof typeof Status]; // "active" | "banned"
    
    function setStatus(s: Status) {}
    setStatus(Status.Active);
    setStatus("banned");
    

    Key Points

    • Unions: zero runtime cost
    • as const objects give runtime values + types
    • Enums emit JS objects
    • const enum issues with isolatedModules
    • Type stripping doesn't support enums

    Permalink

  14. Most asked Which `tsconfig` strictness options matter and what do they catch?

    Summary

    strict: true enables strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, useUnknownInCatchVariables, alwaysStrict and noImplicitThis. Extra recommended flags: noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride and noFallthroughCasesInSwitch.

    Longer Answer

    strictNullChecks is the most valuable: it forces handling null/undefined. noUncheckedIndexedAccess makes arr[i] and record[key] return T | undefined, catching out-of-bounds bugs. When migrating a large JS codebase, enable strict flags incrementally.

    Key Points

    • strict: true bundle
    • strictNullChecks is the biggest win
    • noUncheckedIndexedAccess for index safety
    • Adopt incrementally in migrations

    Permalink

  15. Most asked How do function overloads work in TypeScript and when are they better than unions?

    Summary

    Overloads declare multiple call signatures for one implementation, letting the return type depend on the argument types. The implementation signature must be compatible with all overloads and isn't visible to callers.

    Longer Answer

    Use overloads when the output type depends on input in ways a union can't express (e.g., parse(x: string): Date; parse(x: number): Date[]). Often a generic or conditional return type is simpler. Overload order matters: TypeScript picks the first matching signature.

    Code Example

    function createElement(tag: "a"): HTMLAnchorElement;
    function createElement(tag: "canvas"): HTMLCanvasElement;
    function createElement(tag: string): HTMLElement;
    function createElement(tag: string): HTMLElement {
      return document.createElement(tag);
    }
    
    const c = createElement("canvas"); // HTMLCanvasElement
    

    Key Points

    • Multiple call signatures, one implementation
    • Implementation signature hidden
    • Order matters
    • Consider generics/conditional types first

    Permalink

  16. Most asked Why do you still need runtime validation in TypeScript, and how do libraries like Zod help?

    Summary

    TypeScript types vanish at runtime, so data from APIs, forms, env vars or JSON.parse isn't actually checked. Schema libraries (Zod, Valibot, ArkType) validate data at runtime and infer static types from the same schema, giving one source of truth.

    Longer Answer

    Validate at trust boundaries: incoming requests, external API responses, environment variables and local storage. Use safeParse to handle errors without exceptions. Casting (as User) only silences the compiler; it doesn't make data correct.

    Code Example

    import { z } from "zod";
    
    const UserSchema = z.object({
      id: z.string().uuid(),
      email: z.string().email(),
      role: z.enum(["admin", "user"]),
    });
    type User = z.infer<typeof UserSchema>;
    
    const res = await fetch("/api/me");
    const parsed = UserSchema.safeParse(await res.json());
    if (!parsed.success) throw new Error("Bad payload");
    const user: User = parsed.data;
    

    Key Points

    • Types are erased at runtime
    • Validate at system boundaries
    • z.infer<typeof schema> for types
    • as casts don't validate

    Permalink

  17. Most asked Explain `keyof`, `typeof` (in type position) and indexed access types.

    Summary

    keyof T produces a union of T's property keys; typeof value (in a type position) gets the type of a runtime value; indexed access T[K] looks up the type of a property (or T[number] for array elements).

    Longer Answer

    Together they derive types from values: type Config = typeof config; type Key = keyof Config; type Value = Config[Key]. This keeps types in sync with runtime objects.

    Key Points

    • keyof T → union of keys
    • typeof x → type of a value
    • T[K] and T[number] lookups
    • Derive types from values

    Permalink

  18. Most asked What are type assertions (`as`) and the non-null assertion (`!`), and why should you use them sparingly?

    Summary

    value as T tells the compiler to treat a value as T, and value! asserts it isn't null/undefined. Neither does anything at runtime; if you're wrong, you get runtime errors the compiler could have caught.

    Longer Answer

    Prefer narrowing, type guards or validation. Assertions are acceptable when you genuinely know more than the compiler (e.g., DOM elements guaranteed by the template), but as unknown as T double-casts are a code smell. ESLint rules can restrict them.

    Key Points

    • Compile-time only, no runtime effect
    • Prefer narrowing/validation
    • ! hides possible undefined
    • Avoid as unknown as T

    Permalink

  19. Most asked What are intersection types and how do they differ from interface extension?

    Summary

    Intersection types (A & B) combine types so the value must satisfy all of them. Interface extends also combines but reports conflicts eagerly, whereas conflicting intersection members can silently become never.

    Longer Answer

    Intersections are useful for mixins, adding props (T & { id: string }) and branding. Interfaces with extends usually give better error messages and compiler performance for large object hierarchies.

    Key Points

    • A & B must satisfy both
    • Conflicts may produce never silently
    • extends errors on conflicts
    • Interfaces often faster to check

    Permalink

  20. Most asked What are union types and how do you work with them safely?

    Summary

    A union type (A | B) describes a value that can be one of several types. You can only use members common to all variants until you narrow the value with checks.

    Longer Answer

    Unions model real-world variability: string | number IDs, 'asc' | 'desc' options, or API results. Narrow with typeof, discriminants or guards before using variant-specific members.

    Key Points

    • A | B = one of several types
    • Only shared members usable before narrowing
    • Literal unions replace magic strings

    Permalink

  21. How do you type React component props, children and events in TypeScript?
  22. What are template literal types?
  23. What are covariance and contravariance in TypeScript function types?
  24. What are declaration files (`.d.ts`) and how do you type a JavaScript library without types?
  25. How do you type hooks like `useState`, `useReducer`, `useRef` and context?
  26. How do you write a generic React component (e.g., a typed List or Select)?
  27. How do module systems (ESM vs CommonJS) interact with TypeScript settings?
  28. What are `import type`, `isolatedModules` and `verbatimModuleSyntax`?
  29. How do `tsc`, esbuild, SWC and Babel differ for TypeScript builds?
  30. How would you migrate a large JavaScript codebase to TypeScript?
  31. How does type inference work in TypeScript, and when should you annotate explicitly?
  32. What's the difference between an optional property (`x?: T`) and `x: T | undefined`?
  33. How do optional chaining (`?.`) and nullish coalescing (`??`) work with types?
  34. Explain `void` as a return type and why a callback typed `() => void` can return a value.
  35. How do you model immutability in TypeScript?
  36. How do TypeScript access modifiers compare with JavaScript private fields?
  37. When would you use an abstract class versus an interface in TypeScript?
  38. What are index signatures and how do they relate to `Record`?
  39. How are tuples typed in TypeScript, including labeled and variadic tuples?
  40. Can Node.js run TypeScript directly?
  41. How do path aliases (`@/components`) work, and what's the common pitfall?
  42. What are project references and when do you use them?
  43. What are excess property checks and when do they apply?
  44. Why does `Object.keys(obj)` return `string[]` instead of `(keyof T)[]`?
  45. What's the difference between `// @ts-ignore` and `// @ts-expect-error`?
  46. What changed with decorators in TypeScript 5.0?
  47. What are `using` declarations (TypeScript 5.2)?
  48. What are `const` type parameters (TS 5.0) and `NoInfer` (TS 5.4)?
  49. How do you type JSON values or recursive structures?
  50. How should you handle errors in `catch` blocks in TypeScript?
  51. Why does `Omit` behave unexpectedly on union types, and how do you fix it?
  52. How do you test types themselves?
  53. How do you type an Express/Node API in TypeScript?
  54. How can you share types between frontend and backend?
  55. How does TypeScript type `this`?
  56. How do you keep TypeScript type-checking fast in large projects?
  57. What's the difference between `object`, `Object` and `{}` types?
  58. Why should you avoid the `Function` type?
  59. What is `strictPropertyInitialization` and the definite assignment assertion (`!:`)?
  60. What do `Parameters`, `ConstructorParameters`, `InstanceType` and `ReturnType` give you?
  61. Explain `Exclude`, `Extract` and `NonNullable`.
  62. How would you implement a `DeepPartial<T>` type?
  63. How do you type async functions and promises?
  64. How are generators and iterators typed?
  65. Should you use namespaces in modern TypeScript?
  66. How do you type environment variables safely?
  67. How do you publish a TypeScript library with correct types for ESM and CommonJS consumers?
  68. Can you get TypeScript checking in plain JavaScript files?
  69. What does typescript-eslint add beyond the compiler?
  70. Why are classes with private members compared nominally?
  71. Can you pass a `readonly string[]` to a function expecting `string[]`?
  72. How would you type an event emitter so event names and payloads are checked?
  73. How do you write a type-safe fetch wrapper?
  74. What is the Result pattern in TypeScript and when is it preferable to exceptions?
  75. How do you implement mixins in TypeScript?
  76. How can generics make a builder pattern type-safe (e.g., tracking which fields are set)?
  77. What is a `unique symbol` type?
  78. What do the `target` and `lib` compiler options control?
  79. What are generic defaults and when are they useful?
  80. What are phantom types and how do they relate to branding?
  81. How can `Record<Union, T>` catch bugs when a union grows?
  82. How are getters, setters and the `accessor` keyword typed?
  83. How do you add types to globals like `window`?
  84. What is contextual typing?
  85. What is literal type widening?
  86. What's the difference between `const`, `readonly` and `as const`?
  87. How do you type Maps, Sets and WeakMaps?
  88. Why combine `as const` with `satisfies`?
  89. How does the `in` operator narrow union types?
  90. How would you type a generic repository class?
  91. How do you safely use data from `JSON.parse` without a schema library?
  92. What's the difference between `string[]`, `Array<string>` and `[string]`?
  93. When should a function accept a union parameter versus using overloads?
  94. What is the native TypeScript compiler port (TypeScript 7 / tsgo)?
  95. Why can a function with fewer parameters be assigned to a function type with more parameters?
  96. How do you make a class implement an interface, and what does `implements` actually check?
  97. What do you do when TypeScript infers a generic type parameter too broadly or as `unknown`?
  98. How do you safely work with DOM elements in TypeScript?
  99. What should teams expect from TypeScript 6.0?
  100. What TypeScript practices do you look for in code reviews?