Hard Common TypeScript Interview Questions
1. What are Mixins in TypeScript, and how do they enable composition over inheritance?
Mixins are a pattern to build classes by combining simpler partial classes. Unlike classical inheritance (is-a), Mixins allow a class to use capabilities from multiple sources (has-a). In TypeScript, this is achieved by creating a function that accepts a class constructor and returns a new class extending it with new properties/methods, offering a flexible alternative to deep inheritance hierarchies.
2. In TypeScript, what are the differences between interface and class, and when should you use one over the other?
A Class is a blueprint that generates runtime code (JavaScript functions/prototypes) and can be instantiated. An Interface is a compile-time-only structure used for type checking; it vanishes in the build. Use Interfaces to define data shapes (DTOs) and contracts. Use Classes when you need to create objects that require methods, internal state management, or runtime validation logic.
3. How does TypeScript support Access Modifiers, and how do they translate to the generated JavaScript?
TypeScript provides public, private, and protected. Public is the default. private restricts access to the containing class, and protected allows access in derived classes. Importantly, these are compile-time restrictions only; strict privacy is stripped in the output JavaScript unless utilizing the newer ECMAScript private fields syntax (hash prefix #field).
4. What is the purpose of .d.ts declaration files, and how can they be generated?
Declaration files (.d.ts) describe the shape of an existing JavaScript codebase to TypeScript without containing executable logic. They enable IntelliSense and type checking for JS libraries. They can be generated automatically by the TypeScript compiler using the declaration: true flag in tsconfig.json, which is essential when publishing TypeScript libraries to NPM.
5. Explain Conditional Typing in TypeScript. How is it used to create utility types?
Conditional types take the form T extends U ? X : Y. They allow types to be determined dynamically based on other types. This is the foundation of advanced utility types like Exclude<T, U>, Extract<T, U>, and NonNullable<T>. They enable powerful logic, such as creating a type that flattens a Promise or extracting the return type of a function.
6. Is it possible to create Static Classes in TypeScript? How would you simulate them?
TypeScript does not have a specific static class construct like C#. However, you can achieve the same result by creating a class with a private constructor (preventing instantiation) and only static members. Alternatively, and often preferably in JavaScript/TypeScript, you can simply export constant objects or functions from a module, effectively treating the module itself as a static container.
7. What are Abstract Classes in TypeScript, and how do they differ from Interfaces?
Abstract classes serve as base classes that cannot be instantiated directly. Unlike Interfaces, which are purely for type-checking, Abstract Classes can contain implementation details (code) for some methods while marking others as abstract (forcing derived classes to implement them). They are useful when sharing common logic across a group of related classes.
8. When declaring a class in TypeScript, how does 'Strict Property Initialization' affect field declarations?
When the strictPropertyInitialization flag is enabled (part of strict mode), TypeScript requires that all class properties must be assigned a value either directly at declaration or within the constructor. If a property cannot be initialized immediately (e.g., dependency injection), developers must use the 'definite assignment assertion' operator (!) (e.g., prop!: string) to suppress the compiler error, acknowledging the risk of runtime undefined errors.
9. What are the nuanced differences between the private access modifier and ECMAScript private fields (#)?
The private keyword is a compile-time-only restriction; the property is fully accessible at runtime in the generated JavaScript. In contrast, ECMAScript private fields (prefixing with #, e.g., #hiddenField) provide true runtime privacy. JavaScript engines enforce hard encapsulation, making the field inaccessible and invisible outside the class, which is safer for security-sensitive internal state.
10. What is the specific behavior of the void type when used as a function return type, compared to returning undefined?
A function returning void implies it performs side effects and returns nothing useful. Interestingly, TypeScript allows a variable of type () => void to be assigned a function that does return a value (e.g., Array.prototype.push returns a number but is compatible with void callbacks). This flexibility prevents errors in high-order functions (like forEach). Explicitly returning undefined is stricter and forces the function to return that specific value.
11. What are the specific disadvantages or 'pain points' of using TypeScript in a CI/CD pipeline?
The primary disadvantage is the 'Transpilation Cost'. TypeScript cannot run in the browser or Node.js directly; it must be compiled. In large monorepos, type-checking (running tsc --noEmit) can be slow and memory-intensive, potentially becoming a bottleneck in CI/CD pipelines. Strategies like 'Project References' and incremental builds are required to mitigate this performance hit.
12. What are Mapped Types, and how do they leverage the 'keyof' operator to adhere to the DRY principle?
Mapped Types allow you to create new types by iterating over the keys of an existing type, effectively transforming properties in bulk. Using [P in keyof T], you can modify modifiers (e.g., making all props optional via Partial or readonly via Readonly). This adheres to DRY (Don't Repeat Yourself) by keeping derived types automatically in sync with the source of truth.
13. What are Conditional Types (T extends U ? X : Y) and how are they used to create utility types?
Conditional Types select one of two types based on a test relation, functioning like a ternary operator for the type system. They are the engine behind powerful utilities like Exclude<T, U> and NonNullable<T>. A key feature is 'Distributivity': when applied to a union type, the condition is applied to each member of the union individually, allowing for filtering types from a union.
14. How do you define a recursive type for handling complex structures like JSON objects?
Handling JSON requires recursive types because a JSON object can contain other JSON objects. You can define this as: type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };. This allows the type system to validate deeply nested structures of arbitrary depth.
15. How do you implement Higher-Order Functions (HOFs) in TypeScript while preserving generic type inference?
To preserve types in HOFs (functions that return functions), you must use Generic Type Parameters on the returned function or the outer function scope. A common challenge is that TypeScript often fails to infer types deeply. Best practice involves currying the type parameters or using 'NoInfer' (in newer versions) to guide the compiler. For example, a wrapper function makeSecure<T>(fn: T): T ensures the returned function matches the exact signature of the input function.
16. Explain the concept of Monads in TypeScript. How does the 'Either' or 'Option' pattern improve error handling over try/catch?
Monads (like Option<T> or Either<L, R>) are wrappers that abstract handling of side effects (nulls, errors). Unlike try/catch, which is essentially a 'goto' statement that disrupts control flow and lacks type safety (errors are unknown), Monads force the developer to handle both failure and success cases explicitly at the type level. Libraries like fp-ts use this to create pipelines where errors propagate safely without crashing the app.
17. What is the infer keyword in TypeScript, and how is it used within Conditional Types?
The infer keyword allows you to declare a type variable within a conditional type check to be 'captured' and used in the true branch. It is essential for unwrapping types. For example, ReturnType<T> is implemented as T extends (...args: any[]) => infer R ? R : any. It asks: 'If T is a function, infer its return type as R and give me R'.
18. What strategies can be used to optimize TypeScript compilation performance in a large monorepo?
Key strategies include: 1) Incremental Builds (--incremental) which caches build info to only recompile changed files. 2) Project References (Composite Projects), which split a large codebase into smaller, independent mini-projects that reference each other, allowing TS to skip checking dependencies that haven't changed. 3) skipLibCheck: true, which skips type-checking declaration files (.d.ts) in node_modules, saving significant time.
19. How does TypeScript facilitate safe Server-Side Rendering (SSR) in frameworks like Next.js?
TypeScript bridges the gap between the server (Node.js) and client (Browser) by sharing types. In Next.js, you use specific types like GetServerSideProps to type the data fetching function. The framework then infers the props passed to the page component. The challenge lies in serialization: types must ensure that data sent from the server (e.g., Dates, Maps) is serializable to JSON, or hydration errors will occur.
20. How do you integrate TypeScript with GraphQL to ensure end-to-end type safety?
The gold standard is Code Generation (e.g., GraphQL Code Generator). You should not manually write TS interfaces for GraphQL schemas. Instead, the codegen tool introspects the GraphQL schema and client-side queries, automatically generating TypeScript interfaces and React hooks. This ensures that if the backend schema changes, the frontend build fails immediately, guaranteeing synchronization.
21. What is 'Covariance' and 'Contravariance' in TypeScript, and why does it matter for function assignment?
This relates to type safety when substituting types. TypeScript structural typing is generally Covariant (allows subtypes). However, function parameters are Contravariant when strictFunctionTypes is on. This means if a function expects a Animal, you cannot pass a function that expects a Dog (because the caller might pass a Cat). You can pass a function that expects a Object (supertype). Understanding this prevents subtle runtime bugs in callbacks.
22. How can you utilize TypeScript to prevent 'Prototype Pollution' security vulnerabilities?
While TypeScript is compile-time, it can help prevent pollution by enforcing strict shapes using Object.freeze() or Readonly<T>. More importantly, usage of Map over plain objects ( with __proto__ risks) can be enforced via linting rules. TypeScript's strict null checks also prevent assigning properties to unknown or null objects, which is a common vector for pollution exploits.
23. What is the 'Strangler Fig' pattern in the context of migrating a large JavaScript codebase to TypeScript?
The Strangler Fig pattern involves gradually migrating a system by replacing specific pieces of functionality with new (TypeScript) implementations while the old (JavaScript) system keeps running. In TS migration, this means enabling allowJs: true, then renaming one file at a time to .ts, fixing errors, and defining types. You effectively 'strangle' the untyped legacy code over time without a complete rewrite, which is often too risky.
24. How do you manage versioning and shared types in a Microservices architecture?
There are two main approaches: 1) Monorepo: All services live in one repo and import types directly from a shared-types package. This guarantees immediate consistency but requires unified tooling. 2) NPM Packages: Publish a versioned @org/types package. This decouples services but introduces 'version drift', where Service A and Service B might speak different versions of the data contract. Senior architects often prefer Monorepos or Schema Registries (for things like Protobuf/GraphQL) to solve this.
25. Does TypeScript add overhead to the runtime bundle size? How do you mitigate this?
Generally, types are erased. However, Enums and Decorators generate actual JavaScript code that adds weight. To mitigate this, prefer const enums (which inline values) or Union Types ('A' | 'B') over standard Enums. Also, ensure your tsconfig target is modern (e.g., ES2020); targeting ES5 forces TypeScript to generate heavy polyfill code for features like async/await or classes.
26. How do you handle 'Brand Types' (Nominal Typing) to distinguish between two identical primitive types (e.g., USD vs EUR)?
Since TypeScript is structural, number is compatible with number. To prevent adding USD to EUR, we use 'Branding': type USD = number & { __brand: 'USD' }. The __brand property doesn't exist at runtime, but it forces the type checker to treat USD and EUR as distinct types. You then use a casting function to 'bless' a number as USD. This is critical for financial or scientific applications.
27. In a Microfrontend architecture, how do you handle type sharing and contract versioning between independent applications?
Sharing types in microfrontends is complex because build-time dependencies shouldn't couple runtime deployments. The best approach is publishing a separate 'Design System' or 'Contracts' package containing only .d.ts files and interfaces. Alternatively, using Module Federation, types can be exposed via a remote entry, but this is experimental. Strictly versioning these contract packages ensures that if a Microfrontend updates its API, consumers know immediately via semantic versioning mismatches.
28. How do you ensure Type Safety in large-scale State Management (e.g., Redux/Zustand) without excessive boilerplate?
The key is relying on Type Inference rather than manual annotation. For Redux, use the ReturnType<typeof actionCreator> pattern to generate action types automatically. For modern libraries like Redux Toolkit or Zustand, types are inferred from the initial state and reducers. Senior developers strictly avoid casting (as) inside selectors and instead use utility types to derive the full RootState from the store instance itself, ensuring the state shape is always the single source of truth.
29. What is the distinction between 'Transpilation' and 'Polyfilling' regarding TypeScript targeting older browsers?
TypeScript performs Transpilation (syntax transformation), converting modern syntax (e.g., Arrow Functions, Classes) into older syntax (ES5 functions). However, it does NOT provide Polyfills (missing APIs like Promise, Array.prototype.includes). To support older browsers, you must configure target in tsconfig for syntax, but you must also include a polyfill library (like core-js) to supply the missing runtime objects.
30. How do you optimize a CI/CD pipeline for a massive TypeScript monorepo to avoid 15+ minute build times?
The most effective strategy is splitting the 'Type Checking' and 'Transpilation' processes. Transpilation is handled by fast tools like swc or esbuild (which ignore types and are 20x faster). Type checking is handled by tsc --noEmit but is strictly scoped. By using tools like Nx or Turborepo, the pipeline analyzes the dependency graph and only runs type checks on the specific libraries affected by a PR, rather than the entire repo.
31. How do you approach testing in TypeScript? Specifically, how do you handle 'Type-Safe Mocking'?
In JavaScript, mocks often drift from the actual implementation, causing false positives. In TypeScript, we use utilities like jest.mocked() or vi.mocked() (Vitest) to enforce that the mock implementation matches the original function signature. If the original function changes its arguments, the test code using the mock will fail to compile. This 'Shift Left' approach catches integration bugs before the test suite even runs.
32. Can you discuss the use of TypeScript in Serverless architectures (Lambda/Edge functions)? What are the specific concerns?
In Serverless, 'Cold Start' time is critical. TypeScript can inadvertently bloat bundle size if not tree-shaken correctly, or if huge dependencies (like the full AWS SDK) are imported. The strategy involves using esbuild for aggressive tree-shaking and bundling. Additionally, types are crucial for validating the event payloads (e.g., APIGatewayProxyEvent), as these are external inputs that cannot be validated at compile time without strict schema definitions.
33. What is your strategy for documenting a TypeScript library vs. an application API?
For libraries, TSDoc standard is essential; it allows IDEs to show inline documentation on hover. We also use tools like TypeDoc to generate static HTML documentation directly from the .d.ts files, ensuring docs never rot. For Application APIs, the strategy is 'Code-First': using Decorators (e.g., NestJS Swagger) to generate the OpenAPI/Swagger spec directly from the TypeScript classes, guaranteeing the API documentation matches the actual code.
34. How do you govern a TypeScript project with multiple distinct teams contributing? How do you prevent 'Type Debt'?
Governance requires strict automated linting (ESLint with typescript-eslint) and a strict tsconfig.json. We enforce noImplicitAny and strictNullChecks globally. To prevent 'Type Debt' (developers using as any to bypass rules), we use CODEOWNERS files to require sign-off from a core tech lead whenever a strict rule is disabled or a // @ts-ignore is added. We also track the count of any usages as a metric to be reduced over time.
35. In a distributed system, how do you debug errors when the production code is minified JavaScript but the source is TypeScript?
We use Source Maps, but securely. Production builds generate source maps (.js.map), but they are not uploaded to the public web server. Instead, they are uploaded to the observability platform (Sentry, Datadog, New Relic) during the CI/CD build. This allows the monitoring tool to un-minify the stack trace and show the exact TypeScript file and line number where the error occurred, without exposing source code to the public.
36. How do you implement the 'Singleton' pattern in TypeScript, and why is it often discouraged in modern server-side development?
A Singleton in TS is implemented with a private static instance and a private constructor. However, in modern backend frameworks (Node.js/NestJS), manual Singletons are discouraged because they make testing difficult (global state is hard to reset) and hinder scalability (tight coupling). Instead, we use Dependency Injection (DI) containers which manage the lifecycle of classes, effectively treating them as Singletons within the scope of the application without hard-coding the pattern.
37. What is the 'Ambient Context' anti-pattern in TypeScript, and how do you fix it?
Ambient Context occurs when libraries rely on global objects (like window.MyLib) or implicit contexts that TypeScript cannot see. This breaks type safety. To fix this, we avoid global augmentation. If necessary (e.g., adding user data to an Express Request object), we use 'Declaration Merging' to explicitly extend the interface: declare global { namespace Express { interface Request { user: UserDto } } }. This makes the implicit explicit.
38. How do you automate the migration of a legacy JavaScript project to TypeScript without halting feature development?
We use the 'Loose to Strict' approach. 1) Rename files to .ts allowing implicitAny. 2) Use automated tools like ts-migrate or airbnb-migrate to insert // @ts-expect-error comments on all existing errors. This allows the project to compile immediately. 3) Enforce strict typing on new code only. 4) Use 'ratcheting' scripts in CI that prevent the number of suppression comments from increasing, forcing developers to pay down technical debt gradually.
39. How can TypeScript be utilized in IoT (Internet of Things) or embedded contexts, and what are the limitations?
TypeScript can drive IoT devices via runtimes like DeviceScript or Kaluma (for microcontrollers like ESP32/RP2040). However, standard TypeScript cannot run directly on low-memory hardware. Instead, we use a subset of TypeScript that compiles to highly efficient bytecode or C. The limitation is usually the lack of full JavaScript API support (e.g., no closures or heavy garbage collection) to fit within kilobytes of RAM.
40. What is the relationship between TypeScript and WebAssembly (Wasm)? Can you compile TS directly to Wasm?
Standard TypeScript cannot be compiled to WebAssembly because Wasm requires static memory management, while TS/JS relies on a Garbage Collector. However, AssemblyScript is a strict variant of TypeScript designed specifically to compile to WebAssembly. It looks like TypeScript but enforces stricter typing (e.g., generic integers like i32 vs f64) to map directly to Wasm instructions for near-native performance.
41. How do you achieve end-to-end type safety with Relational Databases (SQL) in a TypeScript backend?
We use Type-Safe ORMs like Prisma or Drizzle. Unlike traditional ORMs (TypeORM/Sequelize) which often rely on classes that can drift from the DB schema, modern tools generate TypeScript definitions directly from the SQL schema or migration files. This means if a column name changes in the database, the backend code fails to compile immediately, preventing runtime SQL errors.
42. Explain 'Const Type Parameters' introduced in TypeScript 5.0. How do they improve API design?
Prior to TS 5.0, functions often needed as const assertions at the call site to infer literal types (e.g., router.get('path', ...)). With Const Type Parameters (<const T>), the modifier is placed on the generic definition itself. This forces the compiler to infer the most specific literal type for arguments automatically, removing the burden from the consumer of the API to write as const.
43. How can you enforce architectural boundaries or specific coding patterns using TypeScript AST (Abstract Syntax Tree)?
Standard linting rules (ESLint) often aren't enough for architectural governance (e.g., 'Domain layer cannot import from Infrastructure layer'). Senior engineers write custom ESLint rules using typescript-eslint. These rules inspect the AST to analyze import paths and class structures, blocking commits that violate the defined software architecture (like Hexagonal or Clean Architecture) before they even reach code review.
44. What is 'Satisfies' operator in TypeScript, and how does it differ from a type annotation?
The satisfies operator (introduced in TS 4.9) validates that an expression matches a type without changing the inferred type of that expression. If you use a standard annotation (const config: Config = ...), you lose specific information (e.g., a specific string becomes just string). satisfies Config checks conformity but keeps the precise literal types, allowing strictly checked but highly specific configuration objects.
45. In a high-performance scenario, how does V8's 'Hidden Classes' concept impact how you write TypeScript classes?
V8 optimizes object access by creating hidden classes (shapes) based on the order properties are initialized. If a TypeScript class adds properties dynamically or in different orders (e.g., using delete or optional properties initialized later), it breaks this optimization chain, forcing V8 into slower dictionary-mode lookups. Senior developers ensure all class properties are initialized in the constructor in a consistent order to maintain high-speed property access.
46. How do you type Dynamic Imports (import()) for Code Splitting to ensure type safety without loading the module?
Dynamic imports return a Promise that resolves to the module namespace object. You can use the import type syntax to reference the shape of a module without bundling it. For example, type MyModuleType = typeof import('./heavy-module');. This allows you to type the resolution of await import('./heavy-module') strictly, ensuring that even lazily loaded components or libraries comply with your application's type contracts.
47. What are Assertion Functions in TypeScript, and how do they differ from Type Guards?
While Type Guards (arg is Type) return a boolean to tell the compiler if a check passed, Assertion Functions (asserts arg is Type) throw an error if the check fails. If the function returns normally, TypeScript narrows the type for the rest of the scope. This is widely used in test utilities (like expect(val).toBeDefined()) or invariant checks to avoid wrapping everything in if blocks.
48. How do Variadic Tuple Types ([...T]) enable typing for higher-order functions like compose or curry?
Variadic Tuple Types allow generics to capture 'the rest' of an array type, preserving the exact order and types of elements. For example, you can define a function that concatenates two arrays as concat<T extends any[], U extends any[]>(arr1: [...T], arr2: [...U]): [...T, ...U]. This is crucial for middleware pipelines (like in Redux or Express) where you need to model the inputs and outputs of a chain of functions dynamically.
49. What is the role of reflect-metadata in TypeScript, and why is it required for frameworks like NestJS or TypeORM?
TypeScript types are normally erased at runtime. However, when emitDecoratorMetadata is enabled in tsconfig, TypeScript emits design-time type information (like the type of a property or constructor parameter) into the generated JavaScript. The reflect-metadata library is a polyfill that allows frameworks to read this emitted metadata at runtime, enabling features like Dependency Injection (inferring which service to inject) or ORM mapping (inferring database column types).
50. How do you handle type definitions for complex Reactive Streams (RxJS) or Event Emitters?
Typing event-driven architectures requires mapping event names to payload types. For RxJS, it involves heavy use of Generics on the Observable<T> class. For Event Emitters, we use a mapped type strategy: interface Events { 'login': User; 'logout': void; }. We then create a typed Emitter class where the on and emit methods use K extends keyof Events to enforce that the data payload matches the event name exactly.
51. What are the risks of using Path Mapping (paths in tsconfig) when publishing a library to NPM?
Path mapping (e.g., @utils/* -> src/utils/*) is great for development ergonomics. However, standard JavaScript environments (Node.js, browsers) do not understand these aliases. If you publish a library with these paths in the output code or declaration files (.d.ts), consumers will crash. Senior engineers use tools like tsc-alias or tspaths during the build process to replace these aliases with relative paths (../../utils) in the final distribution.
52. What are 'Circular Dependencies' in TypeScript, why are they problematic for inference, and how do you resolve them?
Circular dependencies occur when Module A imports Module B, which imports Module A. While JavaScript runtimes might handle this (returning an incomplete object), TypeScript often fails to infer types correctly, leading to implicit any or 'Type alias circularly references itself' errors. To resolve this, senior developers use the 'Barrel File' pattern carefully (though barrels can sometimes cause them), rely on Interface Merging, or inject dependencies via functions/classes rather than importing them directly at the top level.
53. How do you implement 'Polymorphic Components' in React with TypeScript (e.g., a Button that can behave like an Anchor)?
Polymorphic components use a generic as prop (often named as or component). You must use advanced generics to ensure that if as='a' is passed, the component accepts href but not disabled, and if as='button' is passed, it accepts disabled but not href. This is achieved using React.ComponentProps<E> combined with Omit to merge the dynamic element's props with your component's custom props safely.
54. How do you debug 'Type Instantiation is excessively deep and possibly infinite' errors?
This error usually occurs with recursive types (like parsing JSON or deep object updates) that exceed the compiler's depth limit (around 50-100 levels). To fix it, you can simplify the recursion, use interface wrapping (which evaluates lazily) instead of type aliases (which evaluate eagerly), or use specific utility types designed to flatten the recursion structure only when accessed.
55. How do you handle environment variables in Node.js to ensure they are typed and defined at runtime?
Standard process.env is typed as string | undefined. Relying on this leads to 'possibly undefined' checks everywhere. The senior approach is to create a 'Config Service' or use libraries like Zod or Envalid at the application entry point. These libraries validate process.env against a schema at startup. If validation passes, they return a strongly typed configuration object (e.g., config.DB_PORT is guaranteed to be a number), crashing the app immediately if variables are missing.
56. What is the limitation of TypeScript's private fields regarding serializability (JSON.stringify)?
TypeScript's private keyword is compile-time only; the property exists on the object at runtime. If you run JSON.stringify() on an instance, private fields will be included in the JSON output, potentially leaking sensitive internal state. In contrast, ECMAScript private fields (#field) are not enumerable and will not appear in JSON.stringify(), offering better security for API responses.
57. How do you specifically address Memory Management considerations in a long-running Node.js TypeScript application?
While TypeScript compiles to JS (which is garbage collected), type definitions can mask memory leaks. A common senior-level issue is 'Closure Retention' in event listeners or singleton services where large objects are unintentionally kept alive. Developers must explicitly properly untype or nullify references in destroy() lifecycles and avoid storing large datasets in global const maps without a cleanup strategy (e.g., using WeakMap for cache associations).
58. How do you implement the Strategy Design Pattern in TypeScript, and how does it compare to classical OOP implementations?
In classical OOP, the Strategy Pattern involves creating a hierarchy of classes implementing a common interface. In TypeScript, we can simplify this using functional composition. Instead of heavy classes, we define a type Strategy = (input: Data) => Result. We can then create a dictionary of functions (const strategies: Record<Type, Strategy>). This achieves the same polymorphism with significantly less boilerplate and better tree-shaking support.
59. What are the specific challenges of 'Peer Dependencies' when developing and publishing a TypeScript React Component Library?
When publishing a library, you must ensure that react and react-dom are listed as peerDependencies, not dependencies, to avoid bundling duplicate copies of React. In TypeScript, this also applies to types. You must ensure your package.json correctly references types, and that you don't accidentally bundle @types/react in your production build, which can cause 'Duplicate Identifier' errors for the consumer.
60. How do you quantify and systematically reduce 'Type Debt' (usage of any) in a legacy codebase?
We use automated analysis. Tools like type-coverage can generate a numeric percentage of type safety (e.g., '85% typed'). We integrate this into CI/CD, failing the build if the coverage percentage drops (the 'Ratchet' mechanism). Strategically, we prioritize fixing any in 'Core Domain' logic first, while being more lenient with 'UI/Presentation' layers or test files during the transition.
61. Explain the performance impact of 'Megamorphic' vs 'Monomorphic' inline caching in V8, and how TypeScript interfaces relate to this.
V8 optimizes property access if objects always have the same shape (Monomorphic). If a TypeScript function accepts a broad interface (e.g., interface Shape { x: number }) and you pass it objects with different hidden classes (e.g., {x:1, y:2} then {x:1, z:3}), the function becomes 'Megamorphic', forcing V8 to use slow dictionary lookups. Senior developers ensure performance-critical hot paths receive objects with a consistent initialization order and structure.
62. In a Microservices environment, how do you share Data Transfer Object (DTO) types between services without tight coupling?
The preferred pattern is 'Schema-First'. We define the contract using a language-agnostic IDL (like Protocol Buffers or GraphQL/OpenAPI). We then generate TypeScript types for each service from this single source of truth during the build process. This prevents the 'Shared Library' anti-pattern where a change in a shared npm package forces a lock-step deployment of all microservices.
63. How do you optimize TypeScript bundle size for Serverless (AWS Lambda) environments?
Serverless requires minimal cold-start times. We use esbuild for bundling because it performs aggressive tree-shaking. A critical senior optimization is excluding the aws-sdk from the bundle (marking it as 'external') since it is available in the Lambda runtime, saving 50MB+. We also use TypeScript's import type to ensure no design-time dependencies leak into the runtime code.
64. How do you implement 'Integration Testing' involving a database while maintaining Type Safety?
We use Docker to spin up a real database (e.g., Postgres). We then use a tool like prisma migrate to apply the schema. The key for TypeScript is seeding: we create typed 'Factories' (using libraries like factory.ts) that mirror our database entities. This ensures our test data setup is valid at compile-time, preventing 'false negative' test failures caused by bad mock data.
65. What caching strategies can be applied to tsc (TypeScript Compiler) to speed up CI/CD pipelines?
We utilize the .tsbuildinfo file generated by tsc --incremental. In CI, we cache this file based on the hash of the yarn.lock or package-lock.json. However, strictly caching node_modules is more important. For monorepos, we use 'Remote Caching' (available in Nx/Turborepo), where artifacts built by one developer are cached in the cloud and reused by the CI agent instantly.
66. How do you automate API documentation generation from TypeScript code to ensure it never drifts from implementation?
We use a 'Code-First' approach with Decorators. In frameworks like NestJS, we annotate controllers with @ApiProperty or @ApiResponse. At build time, a plugin scans these decorators and the TypeScript types to generate a Swagger/OpenAPI JSON spec. This guarantees that if a developer changes a property type in the DTO, the API documentation updates automatically upon the next build.
67. How do you prevent 'Prototype Pollution' vulnerabilities when writing deep-merge utilities in TypeScript?
Recursive merge functions are a common attack vector. TypeScript helps by allowing us to type the input as Readonly to discourage mutation, but the real fix is runtime validation. We explicitly check for keys like __proto__, constructor, and prototype and block them. Senior developers often prefer using established libraries (like lodash.merge with vulnerability patches) rather than rolling their own, despite the bundle size cost.
68. What is the specific role of core-js when targeting older browsers with TypeScript?
TypeScript transpiles syntax (e.g., let -> var, class -> function), but it does not polyfill features (e.g., Promise, Array.from). core-js provides the actual implementations of these missing standard library features. In a senior setup, we configure @babel/preset-env with useBuiltIns: 'usage' to automatically import only the specific core-js modules required by the code we actually wrote.
69. Can you provide a real-world use case for 'Module Augmentation' involving the Express.js Request object?
Middleware often attaches data (like the authenticated user) to the request. By default, req.user triggers a TypeScript error. We use Module Augmentation to extend the library type: declare module 'express-serve-static-core' { interface Request { user: UserEntity } }. This tells TS that in this project, the Request object always has a user property, enabling autocomplete and type checking downstream.
70. How do you implement Finite State Machines (FSM) using TypeScript Discriminated Unions?
We define the state as a union: type State = { status: 'idle' } | { status: 'loading' } | { status: 'success', data: string }. This prevents impossible states (e.g., having 'error' message while 'loading'). We then use a reducer or switch statement that TypeScript exhaustively checks. This is superior to boolean flags (isLoading, isError) which can accidentally both be true simultaneously.
71. Why might you choose a 'Result Type' pattern (like Rust/Go) over throwing Exceptions for error handling in TypeScript?
Exceptions in JS/TS are not type-safe; a function signature doesn't declare what it throws. By returning a Result<Data, Error> union type, we force the consumer to handle the error case (e.g., checking if (result.isErr())). This makes error handling explicit and compiler-enforced, which is critical for high-reliability systems like payments or critical infrastructure.
72. What are the trade-offs between using swc/esbuild vs tsc for building TypeScript projects?
swc and esbuild are written in Rust/Go and are 10-50x faster than tsc. However, they typically strip types without checking them. The trade-off is that you lose type safety during the build. The standard senior pattern is to use swc/esbuild for the dev server (fast feedback) and production build, but run tsc --noEmit as a parallel check in the CI pipeline to ensure type correctness.
73. How do 'Server Components' (RSC) in Next.js impact TypeScript prop serialization rules?
Server Components render on the server and send a serialized format to the client. This means props passed from a Server Component to a Client Component must be serializable (JSON-compatible). TypeScript cannot strictly enforce this by default (yet), so developers must be vigilant not to pass functions or class instances across this boundary, or the app will crash at runtime.
74. How does TypeScript facilitate 'Schema Stitching' or 'Federation' in a GraphQL Gateway?
In a Gateway, we merge schemas from multiple subgraphs. TypeScript tools (like GraphQL Mesh or Apollo Federation) generate a unified type definition that encompasses all sub-services. This allows the Gateway code to be written with full type safety regarding the combined graph, ensuring that resolvers which cross service boundaries are correctly passing data structures that match the remote service's expectations.
75. How can you implement custom ESLint rules to enforce Architectural Boundaries (e.g., Hexagonal Architecture)?
We use the eslint-plugin-import with no-restricted-paths or dependency-cruiser. We configure rules such that, for example, files in the domain folder cannot import from infrastructure or react. TypeScript's role here is secondary; it provides the AST that ESLint parses. This automated governance prevents 'architectural erosion' where developers take shortcuts that violate the system design.
76. What are the keyof, typeof, and in operators in TypeScript, and how are they used in metaprogramming?
typeof translates a runtime value into a TypeScript type (e.g., type X = typeof MyConfig). keyof produces a union type of all keys in a given object type. The in keyword is used in Mapped Types to iterate over keys in a union (e.g., [K in keyof T]). Together, these operators allow for powerful dynamic type generation, such as creating a type that automatically updates when a configuration object changes, reducing boilerplate.
77. What are Conditional Types and the infer keyword, and how are they used to extract types?
Conditional Types follow the syntax T extends U ? X : Y, acting as a ternary operator for the type system. They allow types to be determined logic-ally based on other types. The infer keyword is used within the condition (the extends clause) to declaratively introduce a new type variable to be 'inferred' from the type being checked. A classic use case is ReturnType<T>, which uses infer R to extract the return type of a function signature.
78. Explain Function Type Variance (Covariance and Contravariance) in the context of TypeScript.
Variance describes how subtypes relate to supertypes. In TypeScript, object properties and function return types are Covariant (a subtype can be assigned to a supertype). However, function arguments are Contravariant (a function expecting a specific type can be assigned to a variable expecting a broader type/supertype). This ensures safety: if a callback expects Animal, it is safe to pass a callback that handles Object, but unsafe to pass one that only handles Dog.
79. What are the satisfies operator and as const assertion, and how do they improve type safety?
as const freezes an object or array at compile time, treating it as a readonly literal type rather than a mutable general type (e.g., converting string to the specific literal 'hello'). The satisfies operator (introduced in TS 4.9) checks that an expression matches a type without changing the resulting type of that expression. This preserves the specific literal types of an object (for inference) while still ensuring it adheres to a wider contract.
80. How do you handle migrating a large JavaScript codebase to TypeScript incrementally?
Incremental migration involves enabling allowJs: true in tsconfig.json to process JS and TS files simultaneously. The strategy usually starts by migrating leaf nodes (utility functions) and working upwards. Using any explicitly is acceptable temporarily to silence errors, but strictness settings (like noImplicitAny) should be turned on gradually. Tools like ts-migrate can automate some annotations. The goal is to establish a 'strict' baseline for new code while gradually typing existing modules.
81. What are Template Literal Types, and how can they be used to create strongly typed string patterns?
Template Literal Types allow the concatenation of literal types at the type level, similar to template strings in JavaScript (e.g., type Event = ``on${Capitalize<Action>}``). This is powerful for modeling patterns like CSS classes, event handlers, or ensuring strings follow a specific format (like hex codes or IDs). It allows the compiler to validate complex string structures that would otherwise require regex checks at runtime.
82. How do Mixins work in TypeScript, and what problem do they solve regarding inheritance?
Mixins offer a way to simulate multiple inheritance or compose behavior into classes. In TypeScript, a mixin is typically a function that takes a base class constructor as an input and returns a new class extending it with added functionality. This pattern is useful for cross-cutting concerns (like adding 'Timestamped' or 'Activatable' capabilities to various distinct models) without creating deep, brittle inheritance chains.
83. Explain Declaration Merging in TypeScript and provide a valid use case for it.
Declaration Merging is a compiler behavior where two separate declarations with the same name are merged into a single definition. This is unique to Interfaces and Namespaces (Classes and Type Aliases do not merge). A primary use case is 'Module Augmentation,' where a developer needs to add properties to an existing third-party interface or a global object. For example, adding a custom property to the global Window interface or extending Express's Request object with user details.
84. How do Mapped Types work, and how can modifiers (- or +) be used to transform properties?
Mapped Types allow you to create new types by iterating over keys of an existing type (e.g., [P in keyof T]: T[P]). Modifiers allow you to add or remove attributes during this mapping. The + (implicit) adds a modifier, while - removes it. For example, [P in keyof T]-?: T[P] creates a type where all optional properties of T become required (removes ?), and -readonly would strip the readonly status from properties, making them mutable.
85. How do Conditional Types work in TypeScript, and how can the infer keyword be used within them?
Conditional types select a type based on a condition, similar to a ternary operator.
Syntax: T extends U ? X : Y
The infer keyword:
It is used within the condition clause to declare a type variable to be deduced. A classic example is getting the return type of a function:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
Here, TypeScript checks if T is a function. If it is, it infers the return type into a new variable R and returns that; otherwise, it returns any. This allows for powerful metaprogramming and dynamic type extraction.
86. Explain the distinction between Type Inference and Contextual Typing with a code example.
Type Inference is when TS deduces the type based on the value assigned.
let x = 3; (TS infers x is number).
Contextual Typing occurs when the type is implied by the location of the expression, typically in callback functions.
Example:
window.onmousedown = function(mouseEvent) { ... };
TypeScript knows mouseEvent is a MouseEvent not because of the value assigned to it, but because the function is assigned to window.onmousedown. If you explicitly typed the variable incorrectly, Contextual Typing would catch the error.
87. How do Function Overloads work in TypeScript, and how does the implementation signature differ from the overload signatures?
Function overloading allows you to define multiple function signatures (overload signatures) for a single function, allowing it to handle different argument types or counts.
Structure:
- Overload Signatures: Declarations of valid ways to call the function. These have no body.
- Implementation Signature: The actual function body. This signature must be general enough to include all overload cases, but it is not directly callable.
Example:
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { ... }
Consumers can only call makeDate with 1 argument or 3 arguments, but not 2, even though the implementation allows it.
88. Explain Mapped Types and provide a practical scenario where they reduce code duplication.
Mapped types allow you to create new types based on old ones by iterating over property keys. Syntax: {[P in keyof T]: U}. This avoids duplication. For example, if you have a User interface, you can create a ReadonlyUser automatically using a mapped type that adds the readonly modifier to every property found in keyof User. It is the mechanism behind standard utility types like Partial<T> and Pick<T, K>.
89. What are Conditional Types, and how does the extends keyword function within them?
Conditional types take the form T extends U ? X : Y. They add logic to the type system, selecting a type based on the relationship between two inputs. A powerful feature of conditional types is 'distributivity': when T is a union type, the condition is applied to each member of the union individually. This allows for advanced type filtering, such as Exclude<T, U>, which filters out types from T that are assignable to U.
90. Define the never type and explain its role in exhaustive type checking.
The never type represents values that never occur, such as a function that always throws an exception or one with an infinite loop. Its most powerful use case is in 'exhaustiveness checking'. In a switch statement over a discriminated union, the default case can be assigned to a variable of type never. If a new case is added to the union later but not handled in the switch, TypeScript will raise a compile-time error because the remaining type is not assignable to never.
91. What is the workflow for utilizing a JavaScript library that lacks both internal types and @types packages?
In this scenario, you must create a declaration file (.d.ts). The simplest approach is a 'shorthand ambient declaration': declare module 'my-lib';, which types all imports from that library as any. For a stricter approach, you define the module and its exported interfaces/functions explicitly within the .d.ts file. This file must be included in the include path of your tsconfig.json.
92. What are Ambient Declarations (declare), and why are they necessary in a TypeScript project?
Ambient declarations (prefixed with declare) tell the TypeScript compiler that a variable, function, or class exists in the global scope or will be provided by the runtime environment, even if it wasn't defined in the current file. They do not emit code. They are necessary for referencing global variables like window, process, or variables injected via script tags (like Google Maps or Analytics globals) without causing compilation errors.
Last updated on
Spotted something unclear or wrong on this page?