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.
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.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.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.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.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.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.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 missingWhat 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.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.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.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.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
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.
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.
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"); // errorWhat 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.
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.
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.
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 existWhat 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.
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.
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 literalWhat 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.
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.
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 {}.
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.
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
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.
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>>; // numberWhat 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.
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 -.
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 AnimalWhat 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.
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); // errorWhat 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.
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.
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.
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?
Is TypeScript tested separately from JavaScript in interviews?
How can I show my TypeScript level if my job used loose compiler settings?
More interview questions
Browse the skills directory- SQL interview questionsJunior to senior, with what a strong answer shows
- Product management interview questionsJunior to senior, with what a strong answer shows
- Python interview questionsJunior to senior, with what a strong answer shows
- React interview questionsJunior to senior, with what a strong answer shows
- System design interview questionsJunior to senior, with what a strong answer shows
- JavaScript interview questionsJunior to senior, with what a strong answer shows
- AWS interview questionsJunior to senior, with what a strong answer shows
- Node.js interview questionsJunior to senior, with what a strong answer shows
- Docker interview questionsJunior to senior, with what a strong answer shows
Practising answers is half of it; the other half is showing the skill. See how Lemma tests TypeScript, how adaptive skills tests work, or how interviewers assess technical skills.

