- TypeScript interviews test whether you can model data with types, not whether you know syntax: unions, discriminated unions, generics with constraints, and utility types.
- Know the crisp distinctions: type vs interface (merging, unions), any vs unknown vs never, annotation vs
asvssatisfies. - Be ready to rebuild utility types live:
Partial,Pick,Omit,ReturnType,Awaited, and aDeepPartial. They are all mapped and conditional types plusinfer. - Exhaustive
switchwith anevercheck is the pattern interviewers love most. - Types disappear at runtime. Validate API input with a schema library and derive types from it.
TypeScript is now the default for serious JavaScript work: React and Next.js frontends at Flipkart, Swiggy and CRED-style product teams, Node.js backends, and most new libraries. Interviews reflect that. Frontend and full-stack rounds increasingly include a TypeScript segment where you type a function, fix a type error, or model an API response. This guide covers 50 questions, and every self-contained snippet here was type-checked under strict mode, so the "this compiles" and "this errors" claims are real.
TypeScript sits on JavaScript semantics, so gaps in closures, this or the event loop will still hurt you. Brush those up with JavaScript and React interview questions.
What TypeScript Interviews Actually Ask
| Level | Typical questions |
|---|---|
| Fresher / SDE-1 | Basic types, interface vs type, any vs unknown, optional properties, enums, simple generics |
| SDE-2 | Narrowing, discriminated unions, generic constraints with keyof, utility types, typing React props and API responses |
| Senior / library authors | Conditional and mapped types, infer, variance, declaration merging, module augmentation, tsconfig trade-offs |
A common live exercise: "Here's a JavaScript function, add types so misuse fails to compile." The examples below are all variations of that.
TypeScript Type System Basics (Q1-9)
Q1. What is TypeScript and what does it add to JavaScript?
A typed superset of JavaScript that compiles to plain JavaScript. It adds static types, checked at compile time, plus editor tooling (autocomplete, refactoring). All types are erased in the output: there is no runtime cost and no runtime checking. A few features do emit code (enums, namespaces, parameter properties), which is why newer tooling such as Node's built-in type stripping prefers "erasable" syntax only.
Q2. What does structural typing mean?
Compatibility is decided by shape, not by declared name. If a value has all the properties a type requires, it is assignable, regardless of what it was declared as. This differs from Java or C#, which are nominal. When you need nominal behaviour (do not mix UserId and OrderId strings), use a branded type:
type UserId = string & { readonly __brand: "UserId" };
const toUserId = (s: string) => s as UserId;
function loadUser(id: UserId) { /* ... */ }
loadUser(toUserId("u1")); // OK
loadUser("raw-string"); // error TS2345: 'string' is not assignable to 'UserId'
Q3. any vs unknown vs never?
| Type | Accepts | You can use it as | Use for |
|---|---|---|---|
any | Everything | Anything (checking switched off) | Migration escape hatch; avoid |
unknown | Everything | Nothing until narrowed | Untrusted input, JSON.parse, caught errors |
never | Nothing | Anything (it never exists) | Impossible states, exhaustive checks, functions that always throw |
function parse(input: unknown): number {
if (typeof input === "number") return input; // narrowed to number
if (typeof input === "string") return Number(input); // narrowed to string
throw new Error("bad input");
}
any is contagious: an any flowing into other variables silently disables checking there too. With useUnknownInCatchVariables (part of strict), catch (e) gives e: unknown.
Q4. What is the difference between void and undefined?
void means "the return value should not be used". A function type returning void can be satisfied by a function that returns something, which is why [1, 2].forEach(x => arr.push(x)) is fine even though push returns a number. undefined is a specific value and type.
Q5. What are literal types and unions?
A literal type is a single exact value: "PAID", 42, true. Unions combine them: type Status = "PENDING" | "PAID" | "FAILED". This is often better than an enum: zero runtime code, great autocomplete, and exhaustiveness checking.
Q6. Enums vs union of string literals?
Enums generate a runtime object; numeric enums also create a reverse mapping and historically accepted any number. String literal unions are erased and simpler. Many teams now prefer unions or a const object:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number]; // "admin" | "editor" | "viewer"
const r: Role = "editor"; // and ROLES is iterable at runtime
const enum is inlined at compile time, but it breaks under single-file transpilers (Babel, esbuild, isolatedModules), which is why it is discouraged in libraries.
Q7. What does as const do?
It infers the narrowest possible type: literal types instead of string/number, readonly properties, and readonly tuples instead of arrays. Useful for configuration objects and for deriving unions from arrays (Q6).
Q8. What are tuples, readonly arrays and optional tuple elements?
type Point = [x: number, y: number]; // labelled tuple
type Range = [start: number, end?: number]; // optional element
const xs: readonly number[] = [1, 2, 3]; // xs.push(4) is an error
React's useState returns a tuple, which is why you can name both elements while destructuring.
Q9. What are excess property checks?
Object literals assigned directly to a typed target cannot have unknown properties, catching typos. The same object assigned via a variable passes, because structurally it has the required fields:
interface Opts { timeout: number }
const bad: Opts = { timeout: 1, retries: 3 }; // error TS2353: 'retries' does not exist
const extra = { timeout: 1, retries: 3 };
const ok: Opts = extra; // allowed: not a fresh literal
Type vs Interface in TypeScript (Q10-15)
Q10. What's the difference between type and interface?
| Capability | interface | type |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions, tuples, primitives, function types | No (functions only via call signatures) | Yes |
| Mapped and conditional types | No | Yes |
| Declaration merging (reopening) | Yes | No: duplicate identifier error |
| Extending | extends, with clear errors on conflicts | Intersection &, conflicts silently become never |
implements by classes | Yes | Yes, if it resolves to an object type |
A good one-line answer: "Use interfaces for object contracts that others might extend, type aliases for unions and type-level computation." The TypeScript handbook has historically suggested defaulting to interface until you need a type-only feature.
Q11. Show declaration merging.
interface User { id: string }
interface User { name: string }
const u: User = { id: "1", name: "Asha" }; // both members required
This is how libraries let you extend their types, and how lib.dom.d.ts adds to global interfaces like Window.
Q12. What is module augmentation? Give a real example.
import "express";
declare module "express-serve-static-core" {
interface Request {
user?: { id: string; roles: string[] }; // set by your auth middleware
}
}
Now req.user is typed in every handler. Similarly, declare global { interface Window { analytics: Analytics } } augments globals. The file must be a module (have an import or export) and be included by your tsconfig.
Q13. extends vs intersection when properties conflict?
interface Base { id: string }
interface Bad extends Base { id: number } // error TS2430: incorrectly extends 'Base'
type X = { id: string } & { id: number }; // no error here, but X["id"] is never
Interfaces fail loudly at the declaration; intersections fail later and more confusingly at use sites. One reason to prefer extends for object hierarchies.
Q14. Interface vs abstract class?
An interface is a compile-time contract with no runtime existence. An abstract class exists at runtime, can contain implementation and constructor logic, and supports protected members. Use an abstract class when subclasses share code; an interface when they only share a shape.
Q15. private vs #private fields?
TypeScript's private is compile-time only; the property is a normal, accessible property at runtime. JavaScript's #field is enforced by the runtime and cannot be accessed outside the class even with casts. Use # when privacy actually matters.
Type Narrowing and Discriminated Unions (Q16-23)
Q16. What are the ways to narrow a type?
typeof x === "string"for primitives.x instanceof Datefor classes."swim" in animalfor property presence.- Equality:
x === null, or comparing with a literal. - Truthiness:
if (user)(careful: also excludes0and""). - Discriminant property checks on unions (Q17).
- User-defined type guards and assertion functions (Q19, Q20).
Q17. What is a discriminated union? Show an exhaustive switch.
type PaymentEvent =
| { kind: "upi"; vpa: string; amount: number }
| { kind: "card"; last4: string; amount: number }
| { kind: "netbanking"; bank: string; amount: number };
function describe(e: PaymentEvent): string {
switch (e.kind) {
case "upi": return `UPI ${e.vpa}`; // e narrowed: vpa exists
case "card": return `Card ****${e.last4}`;
case "netbanking": return `Netbanking ${e.bank}`;
default: {
const _exhaustive: never = e; // compile error if a new kind is added and not handled
return _exhaustive;
}
}
}
Add | { kind: "wallet"; provider: string; amount: number } and the compiler points at the never line until you handle it. This pattern models API responses, Redux actions and state machines; it is the most valuable TypeScript pattern to show in an interview.
Q18. Model a loading state so impossible states can't exist.
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
Compare with { loading: boolean; data?: T; error?: string }, which allows "loading and error and data" all at once. Interviewers ask this to see whether you think in states, not flags.
Q19. What is a user-defined type guard?
interface Fish { swim(): void }
interface Bird { fly(): void }
function isFish(p: Fish | Bird): p is Fish {
return "swim" in p;
}
// if (isFish(pet)) pet.swim(); else pet.fly();
The return type p is Fish tells the compiler what a true result means. The compiler trusts you: a wrong guard is a lie that the type system will believe. Since TypeScript 5.5, simple guards such as arr.filter(x => x !== undefined) are inferred automatically.
Q20. What is an assertion function?
function assertIsDefined<T>(v: T, msg?: string): asserts v is NonNullable<T> {
if (v === undefined || v === null) throw new Error(msg ?? "value is undefined");
}
// const el = document.getElementById("app");
// assertIsDefined(el); // after this line, el is HTMLElement, not HTMLElement | null
Q21. Non-null assertion (!) vs optional chaining (?.) vs nullish coalescing (??)?
x! tells the compiler "trust me, not null", with no runtime check; overuse hides bugs. x?.y short-circuits to undefined at runtime. a ?? b uses b only if a is null or undefined, unlike || which also replaces 0 and "", a real bug source for quantities and prices.
Q22. Type annotation vs type assertion (as)?
An annotation (const u: User = ...) asks the compiler to verify the value. An assertion (value as User) overrides the compiler's judgement; it only refuses when the types are completely unrelated, and as unknown as User forces even that. Assertions on API responses are the most common source of "typed but wrong" production bugs.
Q23. What does satisfies do, and how is it different?
type Route = { path: string; auth: boolean };
const routes = {
orders: { path: "/orders", auth: true },
health: { path: "/health", auth: false },
} satisfies Record<string, Route>;
routes.orders.path; // OK: keys are known exactly ("orders" | "health")
// with ": Record<string, Route>" instead, routes.anything would also type-check
// and the specific keys would be lost
satisfies (TypeScript 4.9+) validates against a type while keeping the inferred, narrower type. Annotation widens, assertion overrides, satisfies checks and preserves.
TypeScript Generics Interview Questions (Q24-30)
Q24. Why generics? Show a constrained generic.
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const name: string = getProp({ id: "1", name: "Asha" }, "name");
// getProp(user, "email") -> error: '"email"' is not assignable to '"id" | "name"'
Generics let a function relate its input and output types. K extends keyof T constrains the key to real properties, and T[K] (an indexed access type) gives the exact property type in return.
Q25. What does keyof do, and typeof in a type position?
keyof T is the union of T's property names. In a type position, typeof value gets the type of a runtime value. Combined: keyof typeof config is the union of a config object's keys, a common way to derive types from data instead of duplicating them.
Q26. What are generic defaults and when does inference fail?
interface ApiResponse<T = unknown> { data: T; status: number } gives a default when none is specified. Inference fails when a type parameter appears only in the return type (function create<T>(): T), so callers must pass it explicitly. NoInfer<T> (TypeScript 5.4) stops a parameter from influencing inference, useful for "default value must be one of the options" APIs.
Q27. Write a type-safe event emitter.
type Events = {
orderPlaced: { orderId: string; amount: number };
userSignedUp: { userId: string };
};
class TypedEmitter<E extends Record<string, unknown>> {
private handlers: { [K in keyof E]?: Array<(payload: E[K]) => void> } = {};
on<K extends keyof E>(event: K, fn: (payload: E[K]) => void): void {
(this.handlers[event] ??= []).push(fn);
}
emit<K extends keyof E>(event: K, payload: E[K]): void {
this.handlers[event]?.forEach((fn) => fn(payload));
}
}
const bus = new TypedEmitter<Events>();
bus.on("orderPlaced", (p) => console.log(p.amount)); // p is fully typed
bus.emit("orderPlaced", { orderId: "o1", amount: 499 });
// bus.emit("orderPlaced", { orderId: "o1" }) -> error: amount missing
This combines a mapped type, keyof constraints and indexed access. It is a favourite live-coding question for senior frontend roles.
Q28. What are const type parameters?
function tuple<const T extends readonly unknown[]>(...xs: T): T { return xs; }
const t = tuple("a", 1); // readonly ["a", 1], not (string | number)[]
Added in TypeScript 5.0, const on a type parameter infers literal types as if the caller wrote as const.
Q29. How do you type function overloads?
function format(x: string): string;
function format(x: number, digits: number): string;
function format(x: string | number, digits?: number): string {
return typeof x === "number" ? x.toFixed(digits) : x.trim();
}
format(" a "); // OK
format(1.234, 2); // OK
// format(1.234) -> error: no overload takes a single number
Callers see only the overload signatures, not the implementation signature. Often a union parameter or a generic is simpler; use overloads when return types differ by input.
Q30. What is variance, briefly?
If Dog is assignable to Animal, is Array<Dog> assignable to Array<Animal>? In TypeScript, yes (arrays are treated covariantly, which is technically unsound because you could push a Cat). Function parameters are checked contravariantly under strictFunctionTypes, except method-style declarations, which remain bivariant for compatibility. Mentioning this unsoundness shows depth; you rarely need more.
TypeScript Utility Types: Use Them and Rebuild Them (Q31-39)
Q31. List the built-in utility types you use most.
| Utility | Does | Example use |
|---|---|---|
Partial<T> | All properties optional | PATCH request bodies |
Required<T> | All required | Config after defaults applied |
Readonly<T> | All readonly (shallow) | Immutable state |
Pick<T, K> / Omit<T, K> | Keep / drop keys | Omit<User, "passwordHash"> for API output |
Record<K, V> | Object with keys K, values V | Record<Status, string> label maps, exhaustive by construction |
Exclude / Extract | Filter union members | Exclude<Status, "PENDING"> |
NonNullable<T> | Remove null and undefined | After validation |
ReturnType / Parameters | Extract from function types | Type a wrapper around a library function |
Awaited<T> | Unwrap promises recursively | Awaited<ReturnType<typeof fetchUser>> |
Q32. Implement Partial, Pick and Omit.
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 PropertyKey> = {
[P in keyof T as P extends K ? never : P]: T[P] // key remapping; never drops the key
};
type O = MyOmit<{ a: 1; b: 2; c: 3 }, "b">; // { a: 1; c: 3 }
These are mapped types: iterate over keys with in, optionally add or remove modifiers. The as clause (key remapping, TypeScript 4.1) can rename or filter keys.
Q33. What do the - and + modifiers do in mapped types?
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // strip readonly
type Concrete<T> = { [K in keyof T]-?: T[K] }; // strip optional (like Required)
Q34. What is a conditional type and what is infer?
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type MyAwaited<T> = T extends PromiseLike<infer U> ? MyAwaited<U> : T;
type R = MyReturnType<() => Promise<number>>; // Promise<number>
type A = MyAwaited<Promise<Promise<string>>>; // string
A extends B ? X : Y is a type-level if. infer declares a type variable captured from the matched structure. Recursion (as in MyAwaited) is allowed.
Q35. What are distributive conditional types?
type ToArray<T> = T extends any ? T[] : never;
type D1 = ToArray<string | number>; // string[] | number[] (distributes)
type ToArrayND<T> = [T] extends [any] ? T[] : never;
type D2 = ToArrayND<string | number>; // (string | number)[] (wrapped: no distribution)
A conditional type over a naked type parameter is applied to each union member separately. That is exactly how Exclude<T, U> = T extends U ? never : T filters unions. Wrapping in a tuple turns distribution off.
Q36. Write DeepPartial and DeepReadonly.
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type DeepReadonly<T> = T extends (...args: any[]) => any
? T // leave functions alone
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
Mention the edge cases: functions, and built-ins like Date or Map that you may want to treat as leaves.
Q37. What are template literal types?
type HttpMethod = "GET" | "POST";
type Endpoint = `${HttpMethod} /api/${string}`;
const ep: Endpoint = "GET /api/orders"; // OK; "PUT /api/x" is an error
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 }
Q38. How would you type a PATCH endpoint body for a User?
Partial<Omit<User, "id" | "createdAt">>: everything optional, but immutable fields cannot be sent at all. Small compositions like this are what interviewers want to see, rather than a hand-written duplicate interface.
Q39. Record<Status, string> vs an index signature?
Record<"PENDING" | "PAID" | "FAILED", string> requires every key: add a status and the compiler forces you to add its label. { [k: string]: string } accepts any key and guarantees none. Prefer finite keys when the set is known.
Advanced TypeScript and tsconfig Questions (Q40-50)
Q40. Which tsconfig flags matter most?
| Flag | Effect |
|---|---|
strict | Enables the strict family, including strictNullChecks, noImplicitAny, strictFunctionTypes, useUnknownInCatchVariables. Non-negotiable for new code. |
noUncheckedIndexedAccess | arr[0] and record[key] become T | undefined. Catches real bugs; not part of strict. |
exactOptionalPropertyTypes | Distinguishes "missing" from "present but undefined". |
isolatedModules | Errors on constructs single-file transpilers cannot handle; needed with esbuild, SWC, Vite. |
skipLibCheck | Skips checking .d.ts files; faster builds, common in apps. |
module / moduleResolution | nodenext for Node packages, bundler for apps built with Vite or webpack. |
Q41. What are .d.ts files and how do you type an untyped library?
Declaration files contain only types. Many libraries bundle them; others have community types in DefinitelyTyped (npm i -D @types/lodash). For a library with none, write a minimal declare module "legacy-lib" { export function track(e: string): void; }, or start with declare module "legacy-lib"; which types it as any.
Q42. How do you validate API responses with types?
import { z } from "zod";
const Order = z.object({
id: z.string(),
amount: z.number().positive(),
status: z.enum(["PENDING", "PAID", "FAILED"]),
});
type Order = z.infer<typeof Order>; // static type derived from the schema
async function getOrder(id: string): Promise<Order> {
const res = await fetch(`/api/orders/${id}`);
return Order.parse(await res.json()); // throws if the server lies
}
A single source of truth for runtime validation and static types is the strongest answer to "TypeScript has no runtime types, so what?"
Q43. How do you type React props and events?
type ButtonProps = {
variant?: "primary" | "ghost";
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick">;
Extending native attributes lets callers pass disabled, aria-* and so on. For generic components (a typed Select<T>), declare the component as a generic function.
Q44. What is the difference between any[], unknown[] and never[]?
any[] disables checks on elements; unknown[] accepts anything but requires narrowing before an element is used; never[] is an array that can never hold a value. You meet never[] in error messages when an empty literal such as { items: [] } is inferred without context and you later try to push into it. The fix is an annotation: const cart: { items: CartItem[] } = { items: [] }.
Q45. What does the keyword infer do in a template literal?
type RouteParams<S extends string> =
S extends `${string}:${infer P}/${infer Rest}` ? P | RouteParams<`/${Rest}`>
: S extends `${string}:${infer P}` ? P
: never;
type P = RouteParams<"/users/:userId/orders/:orderId">; // "userId" | "orderId"
This is how typed routers extract path parameters from a route string. A great "show me something advanced" answer, but explain it step by step.
Q46. What are index signatures and their pitfalls?
{ [key: string]: number } allows any string key. Without noUncheckedIndexedAccess, obj["missing"] is typed number while being undefined at runtime. Prefer Map<string, number> for dynamic dictionaries, or finite Record keys.
Q47. How do decorators work in modern TypeScript?
TypeScript 5.0 implemented the standard ECMAScript decorators proposal, which differs from the older experimentalDecorators used by Angular and NestJS (those frameworks still rely on the legacy flag and emitDecoratorMetadata). Know which one your framework uses; they are not interchangeable.
Q48. What does the using keyword do?
TypeScript 5.2 added explicit resource management: using conn = await pool.connect() (with await using for async disposal) calls the object's [Symbol.dispose] or [Symbol.asyncDispose] when the scope exits, like try-with-resources in Java. Runtime support depends on your target environment.
Q49. How do you migrate a JavaScript codebase to TypeScript?
- Enable checking without converting.
allowJspluscheckJsin a few files, JSDoc types where cheap. - Convert leaf modules first. Utilities and API clients with no internal dependencies, then work upward.
- Turn on strictness incrementally.
noImplicitAnyfirst, thenstrictNullChecks, which finds the most real bugs. - Type the boundaries. API responses with schemas, environment variables with a validated config object.
- Ban new
any. Lint with@typescript-eslint/no-explicit-anyand track remaining ones.
Q50. How is TypeScript compiled, and what's changing?
tsc type-checks and emits JavaScript. Most apps now split the jobs: a fast transpiler (esbuild, SWC, Babel) strips types for builds, while tsc --noEmit runs type checking in CI and the editor. Node.js can now run many .ts files directly by stripping erasable types. The TypeScript team is also porting the compiler to Go for much faster type checking (planned as TypeScript 7); check its status when you interview, since build tooling questions increasingly touch it.
How to Prepare for TypeScript Interviews
Model one domain
Type an e-commerce checkout end to end: discriminated union for payment methods, a FetchState for each request, zod schemas for API responses, Omit/Partial for create and update DTOs.
Rebuild utility types
Write Partial, Required, Pick, Omit, Exclude, ReturnType, Awaited, DeepPartial from memory, and test each with a few assignments that must pass or fail.
Type challenges
Work through the easy and medium problems of the open-source type-challenges repository. Twenty problems cover nearly everything senior interviews ask.
Explain errors out loud
Take three confusing compiler errors from your own code and practise explaining why the compiler is right. Interviewers value that more than clever types.
infer and template literal types only when the problem needs them. The official TypeScript Handbook is the best reference for checking edge cases.TypeScript rarely appears alone: frontend loops pair it with React and CSS (see HTML and CSS interview questions and Next.js interview questions), while backend loops pair it with Node.js (see Node.js interview questions).
Frequently Asked Questions
Prepare for Frontend and Full-Stack Interviews
Prepflix's interview program covers TypeScript, JavaScript and React depth alongside DSA and system design, with mock interviews modelled on Indian product companies.
Explore the Prepflix Course →