17 Senior TypeScript Interview Questions and Answers (2026)

TypeScript is the default choice for typing JavaScript at scale, and most production codebases depend on its type system to catch mistakes before they ship. Plenty of engineers write TypeScript every day without understanding how structural assignability, declaration merging or variance actually work.
This is fine for Junior and Mid level engineers but when it comes to Senior level and above, the expected depth of understanding increases dramatically. Study these questions well and you'll easily stand out from other candidates who don't know how to answer the hard interview questions.
Q1.How does TypeScript determine type compatibility (assignability) between two types?
TypeScript determine type compatibility (assignability) between two types?TypeScript uses structural typing: a type is assignable to another if its shape (members) is compatible, regardless of declared names. Roughly, the source must have at least everything the target requires.
Structural, not nominal:
Compatibility is based on members/shape, not on the name of the interface or class.
If two unrelated types have the same structure, they're interchangeable.
The general rule:
Source is assignable to target if every property the target requires exists in the source with a compatible type.
Extra properties on the source are fine (except excess property checks on literals).
Variance details:
Function parameters are checked bivariantly by default (contravariant under strictFunctionTypes); return types are covariant.
A function with fewer parameters is assignable where more are expected.
Q2.What is global augmentation and what are the risks of using it in a shared codebase?
Global augmentation is adding or extending declarations in the global scope (or a shared global interface) so new members are visible everywhere without an import, typically via declare global. It's powerful but introduces invisible, project-wide coupling.
How it works: Inside a module, declare global { ... } merges into globals like Window, globalThis, or NodeJS.ProcessEnv.
Legitimate uses: Typing genuinely global runtime values: a CDN library on window, env vars, or test framework globals.
Risks in a shared codebase:
No import trail: a global can be modified from any file, so it's hard to find where a type came from.
Collisions: two libraries or modules augmenting the same global can conflict or silently merge wrong.
Encourages global state and weakens encapsulation; refactors become risky.
A loose augmentation (e.g. any props) leaks unsafe types everywhere.
Better alternative: Prefer explicit module exports/imports; reserve global augmentation for things that are truly global at runtime.
Q3.What is the difference between declaration merging and module augmentation?
They're related: declaration merging is the general TypeScript mechanism where multiple declarations with the same name combine into one, and module augmentation is a specific application of it that targets an existing module's exports from outside.
Declaration merging (the general rule):
Same-named interfaces in the same scope merge their members; namespaces and other constructs can also merge.
Happens within your own code or globally.
Module augmentation (a targeted case):
You open another package's module with declare module "pkg" and add/extend its exported types.
The classic use: adding fields to a framework type (e.g. express's Request, or a Redux store type).
The relationship:
Module augmentation is declaration merging applied across module boundaries; it requires the file to be a module (have an import/export).
Global augmentation (declare global) is the same idea aimed at the global scope.
Q4.What is the purpose of triple-slash directives and are they still relevant in modern TypeScript?
Triple-slash directives are special single-line comments starting with /// that give the compiler instructions, most commonly declaring a dependency on another declaration file. They're mostly legacy now, surviving chiefly inside .d.ts authoring.
Common forms:
/// <reference path="..." /> links to another file's declarations.
/// <reference types="node" /> pulls in an @types package's global types.
/// <reference lib="es2020" /> includes a built-in lib.
Why they existed: They were the pre-ES-module way to express file/type dependencies and ordering.
Modern relevance:
In application code, prefer ES imports and tsconfig.json (types, lib, include) instead.
Still useful when authoring .d.ts files that must reference global types (e.g. reference types="node") without importing.
Q5.Explain 'Declaration Merging.' Which construct supports it, and why might it be useful or dangerous when working with third-party libraries?
Declaration merging is TypeScript's ability to combine two or more declarations that share the same name into a single definition. The primary construct that supports it is the interface, which is how you safely extend types you don't own.
Which constructs merge:
interface with the same name merges members (the canonical case).
namespace can merge with another namespace, or with a function, class, or enum to add static members.
Notably, type aliases do NOT merge: a duplicate is an error.
Why it's useful with third-party libraries:
You can add properties to a library's interface (e.g. attach user to a request) without forking it.
Lets you patch missing or evolving type definitions.
Why it can be dangerous:
Silent merges: a typo or accidental same name extends a type instead of erroring, hiding bugs.
You can declare a member exists that the runtime never provides, creating false confidence.
Library upgrades may collide with your augmentation, breaking builds unexpectedly.
Q6.What are assertion functions and the 'asserts' keyword? How do they differ from type predicates using 'is'?
asserts' keyword? How do they differ from type predicates using 'is'?Assertion functions use the asserts keyword to tell the compiler that if the function returns normally (doesn't throw), a condition is guaranteed to hold from that point on. They differ from is type predicates in how they communicate the result: a predicate returns a boolean you branch on, while an assertion narrows the type for the rest of the scope by virtue of not throwing.
Assertion functions:
Signature asserts x is Type or asserts condition; throw if the check fails, otherwise execution continues with the narrowing applied.
Affect code after the call, not inside an if branch.
Type predicates (is): Return boolean typed as x is Type; you must use them in a conditional to get narrowing in that branch.
Key difference: Predicate: "is this true?" you decide what to do. Assertion: "this must be true or I throw" and the rest of the code assumes it.
Q7.How does TypeScript handle module resolution? Explain the difference between CommonJS and ESNext module targets in the context of a modern frontend or backend application.
CommonJS and ESNext module targets in the context of a modern frontend or backend application.TypeScript resolves modules by combining a moduleResolution strategy (how it finds files) with a module target (what import/export syntax it emits). CommonJS uses require/module.exports loaded synchronously, while ESNext emits native import/export that supports static analysis and tree-shaking.
Resolution strategies:
node/node16/bundler mimic how the runtime or bundler walks node_modules, reads package.json fields, and tries extensions.
classic is the legacy relative-only strategy, rarely used today.
CommonJS:
Synchronous require(); the standard for Node.js backends historically.
Dynamic by nature, so it resists static tree-shaking.
ESNext (ES Modules):
Static import/export enables bundlers to drop unused code (tree-shaking): ideal for modern frontends.
Supports import() for lazy/dynamic loading and top-level await.
Practical guidance:
Frontend with a bundler: use module: ESNext and let the bundler handle output.
Node backend: choose CommonJS or NodeNext depending on whether your package.json declares "type": "module".
Q8.Discuss the performance implications of complex recursive types on the TypeScript compiler.
Complex recursive and conditional types are evaluated at compile time, and the compiler must instantiate each step, so deeply recursive types can balloon type-checking time, memory, and editor responsiveness. TypeScript imposes hard limits to protect itself, but you can still hit slow builds long before those limits.
Why it gets expensive:
Each recursive instantiation creates new type objects the checker must compute, cache, and compare.
Conditional + mapped + recursive combinations can multiply work exponentially (e.g. parsing a string type character by character).
Built-in guardrails:
Recursion depth limit (~50 levels) triggers "Type instantiation is excessively deep and possibly infinite."
A cap on the number of instantiations to prevent runaway checks.
Symptoms:
Slow tsc builds and laggy IntelliSense/hover in the editor.
High memory usage in the language server.
Mitigations:
Use tail-recursive patterns and accumulator types so TS can optimize them.
Add explicit base cases and depth limits; prefer interfaces over huge intersection unions.
Diagnose with tsc --extendedDiagnostics and --generateTrace to find hot types.
Q9.What are conditional types in TypeScript? Explain the syntax T extends U ? X : Y and how it allows for dynamic type branching.
T extends U ? X : Y and how it allows for dynamic type branching.Conditional types choose between two types based on a relationship test, written T extends U ? X : Y: if T is assignable to U the type resolves to X, otherwise Y. This brings if/else branching to the type system.
The test is assignability, not equality: extends asks "is T a subtype of U?", so narrower types pass.
Distributive over unions:
When T is a naked type parameter and a union, the check distributes over each member: (A | B) extends U becomes (A extends U) | (B extends U).
Wrap in tuples [T] extends [U] to disable distribution when you want the union treated as a whole.
Combined with infer: The true branch can capture parts of T, enabling extraction like ReturnType.
Q10.Explain Template Literal Types. How can they be used to enforce string patterns like CSS properties or API endpoints at compile time?
Template literal types apply JavaScript template-string syntax at the type level, letting you build new string literal types by interpolating other types: this enforces precise string patterns at compile time.
Syntax mirrors template strings: `prefix-${T}` where T is a string-like type produces the concatenated literal type.
Unions expand combinatorially: If interpolated types are unions, every combination is generated: useful for variants like `${Color}-${Shade}`.
Enforcing API endpoints or routes: type Route = `/api/${string}` only accepts strings with the right prefix, catching typos at compile time.
Pairs with intrinsic string types: Uppercase, Lowercase, Capitalize, and Uncapitalize transform the case inside templates, great for generating on-style event names.
Q11.What is the purpose of the infer keyword within a conditional type?
infer keyword within a conditional type?infer declares a type variable inside a conditional type's extends clause, letting TypeScript pattern-match and capture a piece of a type so you can reuse it in the true branch.
It extracts, it doesn't construct: You place infer R at the position whose type you want, and TypeScript fills R by matching the structure.
Only valid in the conditional's true branch: The inferred variable is in scope after ?, not in the false branch.
Powers many utility types: ReturnType, Parameters, Awaited, and element extraction from arrays all rely on it.
Multiple inferences allowed: Same name in multiple positions infers a union (or intersection in contravariant spots).
Q12.What is the difference between as const (const assertions) and the satisfies operator?
as const (const assertions) and the satisfies operator?Q13.What does it mean for a conditional type to be 'distributive', and how does wrapping a type in a tuple prevent distribution?
Q14.What are decorators in TypeScript? Explain the conceptual difference between the experimental legacy decorators and the new Stage 3 ECMAScript decorators.
Stage 3 ECMAScript decorators.Q15.Explain the concept of branded types (or opaque types) and how you simulate nominal typing.
Q16.Explain variance in TypeScript: what are covariance and contravariance and how do they affect function compatibility?
Q17.What is the 'this' type in TypeScript, and how does polymorphic 'this' help with method chaining?
'this' type in TypeScript, and how does polymorphic 'this' help with method chaining?