TypeScript Interview Questions
All 40 TypeScript interview questions are now live, covering core typing through large-scale usage: generics, utility types, narrowing, tsconfig, React, Node.js, migrations, and compile-time performance.
TypeScript is a typed superset of JavaScript that adds static checking to normal JS syntax.
Teams use it to catch errors before runtime, improve editor support, and refactor large codebases with more confidence.
function total(a: number, b: number): number {\n return a + b;\n}\n\n// total("1", 2); // compile-time error
It helps teams change shared code with fewer production regressions.
Thinking TypeScript changes runtime behavior by itself is wrong.
What kinds of bugs can TypeScript catch early?
Static types describe expected values during development before the code runs.
Type inference lets TypeScript deduce types automatically, while type annotations are explicit type declarations written by the developer.
let count = 3; // inferred as number\nlet name: string = "Amit";\n\nfunction square(n: number) {\n return n * n;\n}
Inference keeps code readable, while annotations document important boundaries like APIs and props.
Over-annotating everything adds noise, but relying on inference everywhere can hide unclear APIs.
Where are annotations more valuable than inference?
any disables type safety and allows any operation, so errors can slip through silently.
unknown is a safe top type that requires narrowing before use, and never represents values that cannot occur, such as impossible branches or thrown functions.
let a: any = "x";\na.toFixed();\n\nlet u: unknown = "x";\nif (typeof u === "string") {\n u.toUpperCase();\n}\n\nfunction fail(message: string): never {\n throw new Error(message);\n}
unknown is useful for external input, while never helps enforce exhaustive checks.
Using any as a shortcut usually removes the exact safety TypeScript was added for.
How would you use never in a switch statement?
Interfaces and type aliases both describe shapes, but interfaces are especially natural for object contracts and extension.
Type aliases are more flexible because they can name unions, intersections, tuples, primitives, and mapped types.
interface User {\n id: string;\n name: string;\n}\n\ntype Status = "idle" | "loading" | "done";\ntype Id = string | number;
Teams often use interfaces for public object contracts and type aliases for composition-heavy utility types.
Treating interface and type as interchangeable in every case misses their different strengths.
When would a type alias work better than an interface?
A union type means a value can be one of several alternatives.
An intersection type combines multiple type requirements into one value that must satisfy all of them.
type Input = string | number;\ntype Timestamped = { createdAt: Date };\ntype User = { id: string; name: string };\ntype AuditUser = User & Timestamped;
Unions model alternate states, while intersections help compose reusable pieces of a domain model.
Assuming a union exposes all properties from every member leads to unsafe property access.
Why do unions usually require narrowing before property access?
Interfaces extend with the extends keyword and support declaration merging, which fits evolving object contracts.
Type aliases usually compose with intersections, which is flexible but can create harder-to-read combined types.
interface Person { name: string; }\ninterface Employee extends Person { employeeId: string; }\n\ntype PersonT = { name: string };\ntype EmployeeT = PersonT & { employeeId: string };
Interfaces are often cleaner for extending domain contracts, while types work well for composing unions and utilities.
Using deep intersections everywhere can make errors harder to understand than interface extension.
Why can declaration merging affect the choice here?
Optional properties may be absent, readonly properties cannot be reassigned after initialization, and index signatures describe dynamic keys.
They are useful for partial data, immutable contracts, and dictionary-like objects, but broad index signatures can weaken precision.
type Settings = {\n theme?: string;\n readonly version: number;\n [key: string]: string | number | undefined;\n};
These features are common in config objects, API payloads, and caches with dynamic keys.
Assuming optional means the property exists with undefined is different from the property being absent entirely.
What downside comes with a very broad index signature?
enum creates a named set of constants that can be numeric or string based.
The trade-off is that enums add runtime output and can be less ergonomic than string literal unions for many frontend and API cases.
enum Role {\n Admin = "ADMIN",\n User = "USER"\n}\n\nconst role: Role = Role.Admin;\n\ntype RoleType = "ADMIN" | "USER";
String unions are often preferred for API-facing values because they are simple and serialize naturally.
Choosing enum by default can add unnecessary runtime code where a union would be enough.
When would you prefer a string literal union over enum?
Strict mode enables a set of stronger type-checking rules such as strictNullChecks and noImplicitAny.
It matters because many subtle bugs come from loose assumptions around nulls, implicit any, and unchecked object access.
// tsconfig.json\n{\n "compilerOptions": {\n "strict": true\n }\n}\n\nfunction greet(name?: string) {\n return name.toUpperCase(); // error under strict mode\n}
Strict settings push teams to model edge cases explicitly instead of discovering them in production.
Turning strict off to silence errors usually hides real design gaps instead of fixing them.
Which strict flag tends to catch the most useful bugs for you?
TypeScript types exist only at compile time and are erased from emitted JavaScript.
Runtime behavior still depends on actual JavaScript values, so external input must be validated even if the static types look correct.
type User = { id: string };\n\nfunction printId(user: User) {\n console.log(user.id);\n}\n\nconst raw = JSON.parse("{\"id\":123}");\nprintId(raw); // compiles if typed loosely, but runtime shape may be wrong
API responses, form input, and JSON parsing always need runtime checks at trust boundaries.
Assuming a type annotation transforms incoming data is a common misunderstanding.
Where do you add runtime validation in a TypeScript app?
Generics let types work with multiple value types while preserving relationships between inputs and outputs.
They are important because they make reusable helpers, collections, and APIs precise without falling back to any.
function first<T>(items: T[]): T | undefined {\n return items[0];\n}\n\nconst a = first([1, 2, 3]);\nconst b = first(["a", "b"]);
Generics power reusable libraries like data fetchers, repositories, and collection utilities.
Using generic type parameters that do not model a real relationship usually adds noise.
How would you constrain a generic to objects with an id field?
keyof gets the keys of a type, typeof captures the type of a value, and indexed access types read a property type from another type.
Together they help build safer utilities that stay aligned with source objects.
const config = { retries: 3, mode: "fast" };\ntype Config = typeof config;\ntype ConfigKey = keyof Config;\ntype Mode = Config["mode"];
These tools are useful for deriving types from config objects and avoiding duplicated declarations.
Duplicating literal object types by hand creates drift that typeof and keyof can avoid.
How would you write a function that only accepts valid keys of an object?
Utility types are built-in helpers for transforming existing types instead of rewriting similar shapes manually.
They are useful for update payloads, selective views, lookup maps, and enforcing required fields in specific contexts.
type User = { id: string; name: string; email?: string };\ntype UserPatch = Partial<User>;\ntype PublicUser = Pick<User, "id" | "name">;\ntype UserWithoutEmail = Omit<User, "email">;\ntype UserMap = Record<string, User>;\ntype CompleteUser = Required<User>;
They reduce repetition when APIs expose create, update, summary, and internal versions of the same entity.
Using utility types blindly can hide domain meaning if the resulting type no longer matches business rules.
When would Partial be unsafe for an API update contract?
Function overloads let you describe multiple valid call signatures for one implementation.
They are useful when return types depend on argument shapes, but they should be used sparingly because unions or generics are often simpler.
function parse(value: string): number;\nfunction parse(value: number): string;\nfunction parse(value: string | number) {\n return typeof value === "string" ? Number(value) : String(value);\n}
Overloads are helpful in library APIs where callers get different typed results from distinct inputs.
Writing many overloads for minor variations often makes APIs harder to maintain than a generic or union-based design.
What is the difference between overload signatures and the implementation signature?
Type guards are runtime checks that let TypeScript narrow a broader type into a more specific one.
Narrowing is how the compiler learns which branch of a union is active after checks like typeof, in, instanceof, or custom predicates.
type Value = string | number;\n\nfunction print(value: Value) {\n if (typeof value === "string") {\n return value.toUpperCase();\n }\n return value.toFixed(2);\n}
They are essential when handling API results, DOM events, and state unions safely.
Accessing union-specific properties before narrowing is a frequent source of type errors.
How do custom type predicate functions work?
A discriminated union is a union whose members share a common literal field such as type or kind.
It is useful because that shared field makes branching explicit, exhaustive, and much easier for the compiler to narrow.
type Result =\n | { kind: "success"; data: string }\n | { kind: "error"; message: string };\n\nfunction handle(result: Result) {\n if (result.kind === "success") {\n return result.data;\n }\n return result.message;\n}
They model async states, reducer actions, and API success or failure responses cleanly.
Using unrelated optional fields instead of a discriminant leads to weaker narrowing and harder maintenance.
How would you enforce exhaustive handling of a discriminated union?
Mapped types transform each property of an existing type, while conditional types choose one type or another based on a relationship test.
Together they enable powerful reusable type transformations, especially in framework and utility code.
type ReadonlyUser<T> = {\n readonly [K in keyof T]: T[K];\n};\n\ntype Message<T> = T extends string ? "text" : "other";
They are common in helper libraries that derive DTOs, readonly views, and filtered property sets.
Very clever type-level programming can quickly become harder to understand than the runtime code it supports.
Can you give an example where a conditional type distributes over a union?
Declaration files describe the type surface of JavaScript code without providing runtime implementation.
They matter because they let TypeScript understand libraries, globals, and modules that are otherwise only runtime JavaScript.
// math-lib.d.ts\ndeclare module "math-lib" {\n export function sum(a: number, b: number): number;\n}
They are essential when consuming older JS packages or publishing typed APIs for others.
Assuming a package is safe just because a .d.ts exists ignores whether the declarations match real runtime behavior.
What is the role of @types packages here?
public members are accessible anywhere, private only inside the declaring class, protected inside the class and subclasses, and readonly prevents reassignment after initialization.
These modifiers shape the class API and communicate intended access, though they do not replace thoughtful object design.
class Account {\n public id: string;\n private balance: number;\n protected currency: string = "USD";\n readonly createdAt = new Date();\n\n constructor(id: string, balance: number) {\n this.id = id;\n this.balance = balance;\n }\n}
They help separate public APIs from internal state in service or domain classes.
Adding classes and modifiers everywhere does not automatically make code better than plain objects and functions.
How is readonly different from deep immutability?
Decorators are annotations applied to classes or class members to attach metadata or wrap behavior.
You should be careful because they rely on specific language and compiler support, can hide control flow, and may create framework coupling.
function sealed(constructor: Function) {\n Object.seal(constructor);\n Object.seal(constructor.prototype);\n}\n\n@sealed\nclass Service {}
They are common in framework-heavy code such as dependency injection or validation metadata.
Using decorators for simple logic can make debugging harder than explicit function calls or composition.
What compiler or runtime concerns come with decorators?
infer lets a conditional type capture part of another type into a temporary type variable.
It is useful for extracting return types, array element types, promise payloads, and other nested pieces without manual duplication.
type Return<T> = T extends (...args: any[]) => infer R ? R : never;\n\ntype A = Return<() => number>; // number
Libraries use infer to build strongly typed wrappers around functions, promises, and schema tools.
Using infer in deeply nested utilities can make diagnostics unreadable if the abstraction is not worth it.
How would you infer the item type from an array?
Template literal types build new string literal types by combining existing literals.
They are useful for typed event names, CSS tokens, route patterns, and API key conventions where string structure matters.
type EventName = "click" | "focus";\ntype HandlerName = `on${Capitalize<EventName>}`;\n\nconst name: HandlerName = "onClick";
They help constrain naming conventions in design systems and event-driven APIs.
Overusing them for every string can slow understanding and sometimes compiler performance.
What built-in helpers commonly pair with template literal types?
Variance describes whether a type relationship is preserved when one type is substituted for another in a generic position.
It matters because function parameters, callbacks, and mutable containers can become unsound if substitution rules are too loose.
type Animal = { name: string };\ntype Dog = Animal & { bark(): void };\n\ntype Handler<T> = (value: T) => void;\n\nconst handleAnimal: Handler<Animal> = a => console.log(a.name);\nconst handleDog: Handler<Dog> = d => d.bark();
Variance shows up when typing event handlers, collections, and library callback APIs.
Assuming all generic types behave the same under substitution leads to subtle callback and mutability bugs.
Why are mutable arrays a classic variance example?
Recursive types refer to themselves, directly or indirectly, to model nested structures like trees, JSON, or comments.
They become risky when the type logic grows too complex, causing slow compilation, hard-to-read errors, or recursion depth limits.
type Json =\n | string\n | number\n | boolean\n | null\n | Json[]\n | { [key: string]: Json };
They are useful for nested documents and component trees, but they need restraint in utility-heavy code.
Building highly recursive meta-types for small productivity gains often creates long-term debugging costs.
How can you simplify a recursive type that is hurting compile speed?
Module resolution is how TypeScript finds the file or package behind an import path.
It depends on project settings, package metadata, path aliases, and the chosen resolution strategy, so a correct import string is not enough by itself.
// tsconfig.json\n{\n "compilerOptions": {\n "baseUrl": ".",\n "paths": {\n "@core/*": ["src/core/*"]\n }\n }\n}\n\nimport { sum } from "@core/math";
Resolution issues appear often in monorepos, path aliases, and mixed ESM/CommonJS projects.
Configuring path aliases in TypeScript alone does not guarantee Node or bundlers can resolve them at runtime.
What problems happen when tsconfig aliases and runtime resolution differ?
These options control emitted JavaScript, available built-in typings, import behavior, aliasing, and how strictly dependency types are checked.
They matter because the wrong tsconfig can create broken builds, missing globals, slow checks, or mismatches between compiler and runtime expectations.
{\n "compilerOptions": {\n "target": "ES2022",\n "module": "NodeNext",\n "lib": ["ES2022", "DOM"],\n "baseUrl": ".",\n "paths": { "@app/*": ["src/*"] },\n "skipLibCheck": true\n }\n}
Good tsconfig defaults keep builds predictable across local development, CI, and production tooling.
Enabling skipLibCheck can improve speed, but treating it as a fix for bad type problems can hide dependency issues.
Which of these options most often causes environment mismatch bugs?
Declaration merging is TypeScript's ability to combine multiple declarations with the same name into a single type definition.
It is especially common with interfaces, namespaces, and global augmentation, but it should be used carefully because implicit merging can be surprising.
interface User {\n id: string;\n}\n\ninterface User {\n name: string;\n}\n\nconst user: User = { id: "1", name: "Amit" };
It is useful when augmenting framework types or extending third-party declarations in controlled ways.
Accidental merging can make types appear from far away files and confuse maintenance.
How is declaration merging different from using intersections?
ESM uses import/export as the standard module system, while CommonJS uses require and module.exports.
The difference matters because TypeScript settings, Node behavior, package.json fields, and emitted output must align with the chosen module format.
// ESM\nimport { readFile } from "node:fs/promises";\nexport const version = "1.0.0";\n\n// CommonJS\nconst fs = require("node:fs");\nmodule.exports = { version: "1.0.0" };
Mixed module setups often break builds, tests, or runtime imports when toolchain assumptions are inconsistent.
Assuming TypeScript will smooth over every ESM/CommonJS mismatch usually leads to runtime import errors.
What project settings help Node understand an ESM TypeScript package?
Start from stable domain concepts, keep transport shapes separate from core models, and compose smaller reusable types instead of building one giant master type.
Scalable typing also means deriving shared pieces, modeling versioned boundaries carefully, and leaving room for runtime validation at external edges.
type ApiUser = { id: string; full_name: string; created_at: string };\ntype User = { id: string; fullName: string; createdAt: Date };\n\nfunction toUser(api: ApiUser): User {\n return {\n id: api.id,\n fullName: api.full_name,\n createdAt: new Date(api.created_at)\n };\n}
Separating API DTOs from domain models prevents backend changes from leaking through the whole frontend or service layer.
Reusing raw API response types as domain types couples business logic to transport details.
How do you decide when a DTO deserves a separate domain type?
Model success and failure explicitly with a result union instead of relying only on thrown exceptions or null values.
This makes call sites handle both paths intentionally and keeps error payloads typed rather than ad hoc.
type Result<T, E> =\n | { ok: true; value: T }\n | { ok: false; error: E };\n\nfunction parseAmount(input: string): Result<number, string> {\n const value = Number(input);\n return Number.isNaN(value)\n ? { ok: false, error: "Invalid number" }\n : { ok: true, value };\n}
Typed results work well for validation, parsing, and service layers where failures are expected outcomes.
Mixing thrown exceptions, null, undefined, and string errors in the same flow makes call sites inconsistent.
When would you still prefer throwing instead of returning a result union?
Migrate incrementally by enabling TypeScript alongside JavaScript, setting realistic compiler strictness, and converting high-value boundaries first.
Focus on modules with shared contracts, frequent bugs, or active development, then tighten settings over time as coverage improves.
{\n "compilerOptions": {\n "allowJs": true,\n "checkJs": true,\n "noEmit": true,\n "strict": false\n },\n "include": ["src"]\n}
A staged migration avoids freezing delivery while steadily improving safety in the most important paths.
Trying to convert everything at once usually creates churn, weak types, and team resistance.
Which modules would you migrate first and why?
Favor types that reflect real domain constraints and developer needs, not clever abstractions for their own sake.
If a type utility is hard to explain, hard to debug, or barely reused, it may be solving a theoretical problem rather than a practical one.
type Status = "idle" | "loading" | "error";\n\ntype User = {\n id: string;\n name: string;\n};\n\n// Prefer this over deeply nested generic meta-types for simple app state.
Simple, explicit types often outperform ultra-generic abstractions in product code maintained by many engineers.
Type cleverness that saves three lines but costs ten minutes of debugging is usually a bad trade.
How do you decide whether a utility type is worth introducing?
Type props from the component boundary inward, keep hook return values explicit when inference becomes unclear, and make context values impossible to misuse.
Good React typing usually means modeling nullable loading states honestly, avoiding overly broad children or event types, and providing small typed hooks for consuming context.
type ButtonProps = {\n label: string;\n onClick: () => void;\n};\n\nfunction Button({ label, onClick }: ButtonProps) {\n return <button onClick={onClick}>{label}</button>;\n}\n\nconst AuthContext = React.createContext<{ user: string | null } | null>(null);
Typed props and context reduce runtime UI bugs when components are reused widely across a product.
Defaulting to broad types like any props or loose context values removes safety exactly where components are shared most.
How would you create a safe custom hook for nullable context?
Keep transport contracts explicit, share stable schemas or types through versioned packages, and separate internal service models from external API surfaces.
For backends, type request data, business objects, and persistence boundaries independently so each layer can evolve without hidden coupling.
type CreateUserRequest = { email: string; name: string };\ntype CreateUserResponse = { id: string; email: string; name: string };\n\nasync function createUser(input: CreateUserRequest): Promise<CreateUserResponse> {\n return { id: "u1", ...input };\n}
Shared contracts reduce integration mistakes between services, web clients, and backend teams.
Sharing internal database shapes across services creates brittle coupling and slows independent evolution.
How do you keep shared contracts versioned safely?
Organize types near the code that owns them, promote only stable cross-cutting contracts into shared packages, and avoid one massive global types folder.
Clear ownership, package boundaries, and naming conventions matter more than inventing a perfect type taxonomy upfront.
packages/\n ui/\n src/button.tsx\n src/button.types.ts\n api-contracts/\n src/user.ts\n web/\n src/features/profile/profile.types.ts
Local ownership keeps refactors smaller, while shared contract packages prevent duplication at real boundaries.
Putting every type in a common folder quickly creates circular dependencies and unclear ownership.
What types belong in a shared package versus staying local?
Start by measuring where time is spent, then reduce work through project references, narrower includes, fewer expensive type patterns, and faster dependency checking settings.
Compile speed usually improves when large projects are split into clear boundaries and type-level complexity is kept under control.
{\n "compilerOptions": {\n "incremental": true,\n "composite": true,\n "skipLibCheck": true\n },\n "include": ["src"]\n}\n\n// Also inspect with: tsc --extendedDiagnostics
Large monorepos often need project references and strict package boundaries to keep feedback loops acceptable.
Blaming TypeScript alone without checking type complexity, dependencies, and project layout misses the real bottleneck.
Which metrics from extendedDiagnostics would you inspect first?
Work from the first meaningful error, reduce the failing example, and inspect the inferred types at each boundary instead of fighting the full message all at once.
Confusing errors often come from a mismatch much earlier in the chain, especially with generics, unions, and overloaded APIs.
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\nconst result: ApiResult<number> = { ok: true, data: 1 };\n\nif (result.ok) {\n result.data.toFixed(2);\n}
Good debugging habits save hours when framework-heavy types generate huge diagnostic messages.
Trying random casts to silence a complex error usually hides the root cause and creates future bugs.
What tools or editor features help inspect inferred types quickly?
Pick one source of truth, automate generation, and ensure runtime validation and static types are derived from the same schema or contract definition.
Sync breaks when teams hand-edit generated files, duplicate models across layers, or skip validation for external input.
const UserSchema = z.object({\n id: z.string(),\n email: z.string().email()\n});\n\ntype User = z.infer<typeof UserSchema>;\n\nconst user = UserSchema.parse(payload);
Schema-first or validation-first pipelines prevent API drift between backend responses and frontend assumptions.
Generated types without runtime validation still trust external data too much, and manual edits to generated files always drift.
What would you choose as the source of truth in your stack?
Set clear compiler and lint rules, document acceptable escape hatches, and enforce them through CI and code review.
Consistency comes from shared engineering policy, not just from enabling strict mode once and hoping everyone uses it well.
{\n "compilerOptions": {\n "strict": true,\n "noUncheckedIndexedAccess": true\n }\n}\n\n// eslint rule examples:\n// @typescript-eslint/no-explicit-any\n// @typescript-eslint/consistent-type-imports
A common baseline prevents one part of the codebase from becoming a weakly typed exception zone.
Allowing silent any usage and inconsistent tsconfig settings across packages creates uneven quality and confusing expectations.
Which escape hatches should require explicit justification in review?
Measure outcomes such as production bug trends, refactor confidence, unsafe cast counts, type coverage, and developer feedback on change safety.
The goal is not maximum type cleverness but whether TypeScript reduces costly mistakes and speeds up safe development over time.
Track metrics such as:\n- number of any casts or ts-ignore comments\n- type coverage by package\n- bugs caused by null or shape mismatches\n- refactor-related regression rate
Meaningful metrics help teams justify TypeScript investment beyond anecdotal editor convenience.
Using only compile success as the metric ignores whether the team is still bypassing safety with any, casts, or weak boundaries.
Which two metrics would best reflect value in your codebase?
Frequently Asked Questions
This page contains the full 40-question TypeScript track across all 5 levels: basic, intermediate, advanced, experienced, and performance.
The live set covers static typing, generics, utility types, unions, narrowing, conditional types, declaration files, module resolution, tsconfig options, React typing, Node contracts, and large-scale team practices.
Yes. The questions cover both browser and server-side TypeScript, including React props, Node backends, shared contracts, API clients, and monorepo organization.
Yes. Every question includes realistic TypeScript snippets, production-oriented design trade-offs, common mistakes, and follow-up prompts.
Yes. The advanced sections cover infer, template literal types, variance, recursive types, module resolution, declaration merging, and ESM vs CommonJS trade-offs.
Yes. The experienced and performance sections cover large JavaScript migrations, shared contracts, compile-time performance, debugging complex errors, and enforcing team standards.
Strong answers explain how type safety improves maintainability and refactoring speed without turning the codebase into unreadable type puzzles.
Because TypeScript types disappear at runtime, so strong candidates know where static safety ends and real JavaScript behavior, validation, and bundling concerns begin.