Skip to content

TypeScript interview questions

TypeScript interviews test whether you can model real data with types and know where the type system stops protecting you. Expect questions on narrowing, generics, unions, strict settings and runtime validation, then type-level work for senior roles. Questions run from junior to senior. Each one says what a strong answer covers and the trap a weak answer falls into.

How Lemma tests TypeScript
TypeScript interview
Mid-level

What is a discriminated union, and how do you make a switch over one exhaustive?

What a strong answer shows

Each member shares a literal tag property, so checking the tag narrows to exactly one member. Assigning the value to never in the default branch turns a forgotten case into a compile error when someone adds a member. A weak answer models the same state with optional fields, which allows impossible combinations.

Junior TypeScript interview questions

  1. 1.What is the difference between a type alias and an interface?

    What a strong answer shows

    Both describe object shapes, but only interfaces merge when declared twice, and only type aliases can name unions, tuples and mapped or conditional types. Interfaces use extends and report conflicting members as errors, while intersections quietly turn conflicting members into never. A weak answer says one is always better instead of naming a team convention and the real differences.

  2. 2.Explain any, unknown and never. When would you use each?

    function parse(json: string): unknown {
      return JSON.parse(json);
    }
    const v = parse("{}");
    v.name; // error: v is unknown
    if (typeof v === "object" && v !== null && "name" in v) {
      v.name; // ok, narrowed
    }

    What a strong answer shows

    any switches off checking, unknown accepts anything but must be narrowed before use, and never has no values. Use unknown for untrusted input and never for impossible branches. The trap is reaching for any to silence an error, which spreads unchecked values through the codebase.

  3. 3.TypeScript is structurally typed. What does that mean in practice?

    What a strong answer shows

    Compatibility depends on shape, not on declared names, so any value with the required members fits a type even if it never mentions it. A strong answer gives a consequence: two different ID types that are both string can be swapped by mistake. A weak answer confuses this with duck typing at runtime, when it is purely a compile-time check.

  4. 4.When should you write type annotations, and when should you let TypeScript infer them?

    What a strong answer shows

    Let inference handle local variables and simple returns, and annotate function parameters, public exports and any value whose inferred type would be too wide. Annotating return types on exported functions keeps the contract stable and the errors close to the cause. Annotating everything adds noise, and annotating nothing on a library surface leaks accidental types.

  5. 5.How do you narrow a union type?

    What a strong answer shows

    Name the built-in guards: typeof for primitives, instanceof for classes, the in operator for properties, equality checks and truthiness. A strong answer notes that a truthiness check also removes 0 and the empty string, which is a common bug. It also knows that narrowing is lost inside callbacks if the variable can be reassigned.

  6. 6.What is the difference between an optional property and a property typed as string | undefined?

    type A = { name?: string };
    type B = { name: string | undefined };
    
    const a: A = {}; // ok
    const b: B = {}; // error: name is missing

    What a strong answer shows

    An optional property may be left out entirely, while a property typed with undefined must be present, even if its value is undefined. With exactOptionalPropertyTypes on, an optional property also refuses an explicit undefined. Weak answers treat the two as identical, which matters for spreads, Object.keys and APIs that distinguish missing from cleared.

  7. 7.Would you use an enum or a union of string literals, and why?

    What a strong answer shows

    A string literal union has no runtime cost, works with plain JSON values and narrows cleanly. Enums emit runtime code, numeric enums accept any number in some cases, and const enums break under isolated transpilers. A strong answer notes that a runtime list of values is also possible with an as const object.

  8. 8.What happens to your types when the code runs?

    What a strong answer shows

    Types are erased, so you cannot check an interface at runtime or trust a typed value that arrived over the network. Only a few features emit JavaScript, such as enums, namespaces and parameter properties. A strong answer adds that Node.js can now strip types and run .ts files directly, which is why erasableSyntaxOnly exists.

  9. 9.What do readonly and as const do?

    What a strong answer shows

    readonly stops reassignment of a property or array through that type, and as const makes a literal deeply readonly with the narrowest literal types. A strong answer stresses both are compile-time only and shallow in the case of readonly. It also uses as const to derive a union, such as typeof ROLES[number].

  10. 10.What is the risk of using as and the non-null assertion operator?

    What a strong answer shows

    Both tell the compiler to trust you, so a wrong assertion moves the error to runtime with no warning. A strong answer prefers narrowing or validation and keeps assertions at a single, checked boundary. The trap is using as to make a fetch response fit a type, which proves nothing about the data.

Mid-level TypeScript interview questions

  1. 11.What is a discriminated union, and how do you make a switch over one exhaustive?

    type Result =
      | { status: "ok"; data: string }
      | { status: "error"; error: Error };
    
    function handle(r: Result): string {
      switch (r.status) {
        case "ok": return r.data;
        case "error": return r.error.message;
      }
      const unreachable: never = r; // fails if a case is missing
      return unreachable;
    }

    What a strong answer shows

    Each member shares a literal tag property, so checking the tag narrows to exactly one member. Assigning the value to never in the default branch turns a forgotten case into a compile error when someone adds a member. A weak answer models the same state with optional fields, which allows impossible combinations.

  2. 12.How do user-defined type guards and assertion functions work?

    What a strong answer shows

    A guard returns value is T and narrows in the true branch, while asserts value is T narrows after the call or throws. The compiler trusts the predicate, so a wrong body is silently unsound. A strong answer also knows TypeScript 5.5 infers simple predicates, so filter(x => x !== undefined) now narrows.

  3. 13.Write a generic function that reads a property from an object safely. Why use a constraint?

    function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
    const user = { id: 1, name: "Ada" };
    const n = getProp(user, "name"); // string
    getProp(user, "email"); // error

    What a strong answer shows

    The key parameter is constrained with extends keyof T, so only real keys are accepted and the return type is the indexed access T[K]. Constraints let you use members of a type parameter while keeping the caller's specific type. A weak answer types the key as string and returns any, losing both safety and inference.

  4. 14.Which built-in utility types do you use most, and what are their limits?

    What a strong answer shows

    Expect Partial, Required, Pick, Omit, Record, ReturnType, Parameters and Awaited, each tied to a real use such as a patch payload. A strong answer knows Omit accepts keys that do not exist and does not distribute over unions. It also knows Partial is shallow, so nested objects stay fully required.

  5. 15.How do keyof, typeof and indexed access types work together?

    What a strong answer shows

    typeof lifts a value into the type world, keyof lists an object type's keys, and T[K] reads the type at a key. Together they derive types from one source object, so config and types cannot drift. The trap is confusing JavaScript typeof at runtime with the type query in a type position.

  6. 16.What does the satisfies operator do that a type annotation does not?

    type Route = { path: string; auth: boolean };
    
    const routes = {
      home: { path: "/", auth: false },
      account: { path: "/account", auth: true },
    } satisfies Record<string, Route>;
    
    routes.account.path; // ok
    routes.acount; // error: key does not exist

    What a strong answer shows

    satisfies checks a value against a type but keeps the narrower inferred type, so specific keys and literals survive. An annotation widens the value to the declared type, so a typo in a key lookup still compiles. A weak answer treats satisfies as a cast, but it never changes the type and still reports excess properties.

  7. 17.What does strict mode turn on, and which extra flags would you add?

    What a strong answer shows

    strict enables strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, useUnknownInCatchVariables and related checks. A strong answer adds noUncheckedIndexedAccess, which is not part of strict and makes array and record lookups possibly undefined. It names the cost too: more narrowing code in exchange for fewer runtime crashes.

  8. 18.Why does TypeScript reject an extra property in an object literal but accept the same object through a variable?

    type Options = { timeout?: number; retries?: number };
    
    const a: Options = { timeout: 100, retires: 3 }; // error: excess property
    const raw = { timeout: 100, retires: 3 };
    const b: Options = raw; // ok: not a fresh literal

    What a strong answer shows

    Excess property checking applies only to fresh object literals, because an unknown key there is likely a typo. Once the object is held in a variable, plain structural compatibility applies and extra members are allowed. A strong answer connects this to optional options objects, where a misspelt key would otherwise pass silently.

  9. 19.When would you use function overloads instead of a union parameter or a generic?

    What a strong answer shows

    Overloads fit when the return type depends on which input form was passed and a generic cannot express the link. Callers see only the overload signatures, and the implementation signature must be compatible with all of them. A strong answer prefers a generic or a single union where possible, because overloads are checked loosely against the body.

  10. 20.What are declaration files, and how do you type a library that ships without types?

    // types/env.d.ts
    declare global {
      namespace NodeJS {
        interface ProcessEnv {
          DATABASE_URL: string;
        }
      }
    }
    export {};

    What a strong answer shows

    A .d.ts file describes types with no implementation, and most untyped packages are covered by @types packages. Otherwise you write declare module for the package, or augment an existing module or global interface. A strong answer knows a file with no import or export is a global script, so augmentations need an export {}.

  11. 21.Your API response is typed, so why would you still validate it at runtime?

    import { z } from "zod";
    
    const User = z.object({
      id: z.string(),
      age: z.number().int().nonnegative(),
    });
    type User = z.infer<typeof User>;
    
    const parsed = User.safeParse(await res.json());
    if (!parsed.success) throw parsed.error;

    What a strong answer shows

    Types describe what you expect, not what arrives, so data crossing a boundary must be parsed. A schema library such as zod validates the value and derives the static type from the same schema with z.infer. A weak answer writes the interface and the validator separately, so they drift, or casts the JSON and hopes.

  12. 22.What do the module and moduleResolution settings control, and which would you choose?

    What a strong answer shows

    They decide how imports are emitted and how specifiers are resolved to files and package exports. Code that Node.js runs directly should use nodenext, which requires file extensions in relative ESM imports. Code that goes through a bundler can use bundler resolution, and verbatimModuleSyntax makes type-only imports explicit.

Senior TypeScript interview questions

  1. 23.How do conditional types distribute over unions, and how do you stop it?

    type ToArray<T> = T extends unknown ? T[] : never;
    type A = ToArray<string | number>; // string[] | number[]
    
    type ToArrayOnce<T> = [T] extends [unknown] ? T[] : never;
    type B = ToArrayOnce<string | number>; // (string | number)[]

    What a strong answer shows

    A conditional type on a naked type parameter is applied to each union member separately, and the results are unioned. Wrapping both sides in a tuple turns distribution off. A strong answer also knows never distributes to never, which surprises people writing an IsNever check.

  2. 24.What does infer do? Write a type that extracts the element type of an array.

    type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
    type Unwrap<T> = T extends Promise<infer V> ? V : T;
    
    type E = ElementOf<readonly string[]>; // string
    type U = Unwrap<Promise<number>>; // number

    What a strong answer shows

    infer declares a type variable inside the extends clause of a conditional type, which TypeScript fills by matching. It is how ReturnType, Parameters and Awaited are built. A strong answer handles readonly arrays and knows Awaited unwraps nested promises recursively, unlike a naive one-level version.

  3. 25.Write a mapped type that turns each property into a getter method.

    type Getters<T> = {
      [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
    };
    
    type G = Getters<{ name: string; age: number }>;
    // { getName: () => string; getAge: () => number }

    What a strong answer shows

    A mapped type iterates keyof T, and the as clause remaps each key, here with a template literal type and Capitalize. The string & K intersection is needed because keys can also be numbers or symbols. A strong answer mentions the readonly and optional modifiers, which can be added or removed with + and -.

  4. 26.Explain covariance and contravariance in TypeScript with an example.

    class Animal { name = ""; }
    class Dog extends Animal { bark() {} }
    
    let takesAnimal = (a: Animal) => {};
    let takesDog = (d: Dog) => d.bark();
    
    takesDog = takesAnimal; // ok: parameters are contravariant
    takesAnimal = takesDog; // error under strictFunctionTypes
    
    const dogs: Dog[] = [];
    const animals: Animal[] = dogs; // allowed, unsound if you push an Animal

    What a strong answer shows

    Return types and readonly data are covariant, while function parameters are contravariant under strictFunctionTypes. A strong answer knows method shorthand stays bivariant even in strict mode. It also knows mutable arrays are treated as covariant, which is unsound for writes.

  5. 27.How would you stop a UserId from being passed where an OrderId is expected?

    type UserId = string & { readonly __brand: "UserId" };
    type OrderId = string & { readonly __brand: "OrderId" };
    
    function toUserId(s: string): UserId {
      if (!s.startsWith("u_")) throw new Error("Not a user id");
      return s as UserId;
    }
    
    declare function getUser(id: UserId): void;
    getUser(toUserId("u_1")); // ok
    getUser("o_1" as OrderId); // error

    What a strong answer shows

    Structural typing treats both as string, so you add a brand: an intersection with a unique phantom property. The only way to create the branded type is a constructor function that validates once and casts. A strong answer weighs the friction against the bugs prevented and keeps brands to identifiers and units.

  6. 28.Your build uses esbuild or SWC. What does that change about how you use TypeScript?

    What a strong answer shows

    Those tools transpile one file at a time and never type-check, so tsc --noEmit must run separately in CI. Enable isolatedModules so the compiler rejects patterns that need whole-program knowledge, such as re-exporting a type without export type. A strong answer avoids const enums across files and uses explicit type-only imports.

  7. 29.Type-checking in a large codebase has become slow. How do you investigate and fix it?

    What a strong answer shows

    Measure first with tsc --extendedDiagnostics and --generateTrace to find the files and types that dominate. Typical fixes are explicit return types on exports, interfaces instead of large intersections, and limiting deep recursive conditional types. At monorepo scale, project references and incremental builds stop every package rechecking on each change.

  8. 30.How would you move a large JavaScript or loosely typed codebase to strict TypeScript?

    What a strong answer shows

    Go incrementally: allowJs, then convert files at the edges, then turn on strict flags one at a time with the error count tracked. Use @ts-expect-error rather than @ts-ignore, because it fails once the underlying error is fixed. A weak plan is a big-bang rewrite, or enabling strict and casting to any until it compiles.

Prove it instead of claiming it.

Answers show you can talk about TypeScript. A dated test result and live sessions on a Skills Passport show you can do it. Lemma is in early access.

TypeScript interviews: common questions

Do TypeScript interviews include type-level puzzles?

Some senior rounds ask you to write a mapped or conditional type, such as a deep partial or a key extractor. Most interviews spend far more time on modelling data with unions, narrowing and generics in ordinary application code. Practise reading compiler errors aloud, because explaining the error matters as much as fixing it.

Is TypeScript tested separately from JavaScript in interviews?

Usually not. Interviewers expect TypeScript answers to rest on JavaScript fundamentals such as closures, promises and how this works. Many questions are about where the two meet, such as type erasure, runtime validation and what the compiler emits.

How can I show my TypeScript level if my job used loose compiler settings?

Build or contribute to a project with strict mode and noUncheckedIndexedAccess on, and be ready to explain the errors you fixed. Interviewers value that more than a list of utility types. Lemma's adaptive skills test for TypeScript asks 15 to 25 questions that adjust to your answers and records a dated level you can share.