Medium Common TypeScript Interview Questions
1. Analyze the 'Enum' type in TypeScript: types of Enums, compilation output, and when to use them.
Enums allow the definition of a set of named constants.
Types:
- Numeric Enums: Auto-incrementing numbers (0, 1, 2...). They support reverse mapping (Value -> Key).
- String Enums: Keys map to specific string values. Easier to debug but do not support reverse mapping.
- Heterogeneous Enums: Mix of strings and numbers (discouraged).
Performance & Compilation:
Standard Enums compile into an IIFE (Immediately Invoked Function Expression) object in JavaScript, which adds code to the bundle. To optimize performance, use const enum, which erases the Enum definition during compilation and inlines the values at the call sites, reducing bundle size.
2. How do functions in TypeScript differ from JavaScript, specifically regarding Overloads and Parameter handling?
TypeScript adds strict contract enforcement to functions.
Key Features:
- Optional Parameters: Defined using
?(e.g.,function(a: string, b?: number)). These must come after required parameters. - Default Parameters: Supported similarly to ES6 (
param = value), and TypeScript infers the type automatically. - Function Overloads: TypeScript allows defining multiple signatures for a single function implementation. This provides type safety for functions that return different types based on input arguments. The implementation signature itself is not visible to the caller, only the overload signatures are.
3. Explain 'Type Inference' in TypeScript and how the 'Best Common Type' algorithm works.
Type Inference is the compiler's ability to automatically determine the type of a variable based on its initialization, removing the need for explicit type annotations.
Best Common Type Algorithm: When inferring types from an array or a set of values, TypeScript looks for a type that is the supertype of all candidates.
- Example:
let x = [0, 1, null];TypeScript considersnumberandnull. If strict null checks are off, it infersnumber[]. If strict null checks are on, it may infer(number | null)[]. This feature promotes cleaner code by reducing verbosity while maintaining type safety.
4. Describe the TypeScript compilation process and the role of tsconfig.json.
TypeScript code cannot be executed directly by browsers or Node.js. The TypeScript Compiler (tsc) performs two main tasks:
- Type Checking: It statically analyzes the code for type errors.
- Transpilation: It converts TS code into valid JavaScript (removing type annotations, interfaces, etc.) targeting a specific ECMAScript version (e.g., ES5, ES6).
tsconfig.json:
This file is the root configuration for the project. It defines:
- Compiler Options: Target version (
target), module system (module), strictness levels (strict), and output directories (outDir). - File Inclusion: Which files to compile (
include,exclude).
5. How do TypeScript Classes enhance ES6 Classes, and what are 'Parameter Properties'?
TypeScript builds upon ES6 classes by adding type safety and access control.
Enhancements:
- Access Modifiers:
public,private, andprotectedcontrol visibility. (Note: standard JS now has#for private fields, but TS modifiers are compile-time checks). - Abstract Classes: Classes that cannot be instantiated and serve as base templates.
- Readonly Properties: Properties that cannot be changed after initialization.
Parameter Properties: A shorthand syntax to declare and initialize class properties in the constructor.
// Shorthand
constructor(public name: string) {}
// Equivalent to:
// public name: string;
// constructor(name: string) { this.name = name; }6. Explain Inheritance in TypeScript and the distinction between ES6 class inheritance and prototypal inheritance.
Inheritance allows a child class to derive properties and behavior from a parent class using the extends keyword. It enables code reuse and polymorphism.
ES6 vs. Prototypal:
- ES6/TS Class Inheritance: Uses
class Child extends Parentandsuper(). It is syntactic sugar that makes inheritance declarative and readable, aligning with classical OOP languages. - Prototypal Inheritance: The underlying mechanism of JavaScript. Objects inherit directly from other objects via the prototype chain. Before ES6, developers manually manipulated
prototypeobjects. TypeScript compiles Class syntax down to Prototypal inheritance when targeting older JS versions (ES5), ensuring compatibility while offering modern syntax.
7. What are Abstract Classes in TypeScript, and how do they differ from Interfaces?
Abstract Classes act as base templates for other classes. They can contain both implementation details (concrete methods) and abstract methods (signatures without implementation) that derived classes must implement.
VS Interfaces:
- Code Reuse: Abstract classes allow you to provide common implementation details (code reuse) to subclasses. Interfaces only define the structure (contract) and contain no implementation.
- instantiation: Neither can be instantiated directly.
- Usage: Use Abstract Classes when there is a strict 'is-a' relationship and shared logic. Use Interfaces when defining a common functionality across disparate objects (e.g.,
CanFlyfor both aBirdand aPlane).
8. How does TypeScript handle null and undefined, and how does the --strictNullChecks flag impact code safety?
By default, null and undefined are subtypes of all other types. However, when the --strictNullChecks flag is enabled (highly recommended), they become distinct types. This forces developers to explicitly handle null cases (e.g., string | null), significantly reducing ReferenceError and TypeError (e.g., cannot read property of null) at runtime.
9. What is the in operator used for in TypeScript, and how does it act as a Type Guard?
The in operator checks for the existence of a property within an object. In TypeScript, it serves as a Narrowing Type Guard. If we check if ("role" in user), TypeScript knows that within that block, the user object possesses the role property, allowing safe access to it even if user was previously a broad union type.
10. What are Type Aliases and how do they differ from Interfaces?
Type Aliases (type Name = ...) create a new name for a type (primitive, union, intersection, etc.). Interfaces (interface Name { ... }) define the structure of objects. Key differences: Interfaces support 'Declaration Merging' (adding properties to an existing interface) and extend classes, while Type Aliases are more flexible for defining Unions, Tuples, and complex utility types. Use Interfaces for libraries/API definitions, and Aliases for complex type compositions.
11. Is TypeScript considered a strictly statically typed language? Explain the nuances.
TypeScript is statically typed, but it is structurally typed (duck typing) rather than nominally typed (like Java/C#). It is also gradually typed, meaning explicit types are optional and type inference is used. Furthermore, the existence of any allows developers to opt-out of the static system. Therefore, it is a structural, gradual type system that compiles to dynamic JavaScript.
12. Explain the noImplicitAny compiler option. Why is it critical for maintaining code quality?
noImplicitAny flag ensures that TypeScript raises an error whenever it cannot infer a type and falls back to any. Without this, variables implicitly become any, silencing type errors and defeating the purpose of TypeScript. Enabling this flag is the first step in maintaining a strict, type-safe codebase.
13. What is the never type in TypeScript, and what are its practical use cases in exhaustive type checking?
never represents values that never occur (e.g., a function that always throws an error or has an infinite loop). A powerful senior-level use case is 'Exhaustive Checking' in switch statements: assigning the remaining case in a union type check to never. If the union is expanded later but the switch isn't updated, TypeScript will throw a compile-time error.
14. Explain Enums in TypeScript. What is the difference between numeric, string, and const enums?
Enums allow defining a set of named constants. Numeric enums auto-increment; String enums provide readable values for debugging. 'Const enums' are completely erased during compilation, with their values inlined at usage sites, offering performance benefits by reducing code size. Standard enums generate a reverse-mapping object in JavaScript, which adds runtime overhead.
15. What is Type Inference in TypeScript, and when should a developer rely on it versus explicit annotation?
Type Inference is the compiler's ability to deduce types based on values (e.g., let x = 10 infers number). Best practice is to rely on inference for local variables and simple expressions to reduce verbosity ('noise'). Explicit annotations should be reserved for function parameters, return types of complex functions, and public API definitions to ensure contracts don't accidentally change due to implementation details.
16. What is the role of tsconfig.json and what are the implications of the target and module settings?
tsconfig.json is the root configuration determining how TypeScript compiles to JavaScript. target dictates the ECMAScript version of the output (e.g., ES5 for older browsers, ESNext for modern environments). module determines the module system used in the output (e.g., CommonJS for Node.js, ESNext for modern bundlers). incorrect settings here can lead to runtime environment incompatibilities.
17. What are Decorators in TypeScript, and how are they used in frameworks like Angular or NestJS?
Decorators are a meta-programming feature (currently experimental/stage 3) that allows annotating classes, methods, accessors, or properties to modify their behavior. They function as wrappers. In frameworks like NestJS, they are heavily used for Dependency Injection (@Injectable), routing (@Controller), and metadata association, enabling a declarative coding style.
18. How do you combine multiple TypeScript files into a single JavaScript bundle?
While the TypeScript compiler (tsc) can output a single file using the --outFile flag (mostly for SystemJS/AMD modules), modern development relies on bundlers like Webpack, Rollup, or Esbuild. These tools resolve ES Modules (import/export), tree-shake unused code, and bundle assets into a single artifact for deployment.
19. Does TypeScript support Template Literals, and how has this feature evolved into Template Literal Types?
Yes, TypeScript fully supports ES6 Template Literals (backticks). More importantly, TypeScript 4.1 introduced 'Template Literal Types', which allow for powerful string manipulation at the type level. For example, you can construct new types by concatenating string literal types (e.g., type EventName = ``${Module}_${Action}``;), enabling highly specific pattern validation for API endpoints or event handlers.
20. How do Modules work in TypeScript, and why are Namespaces considered legacy?
Modern TypeScript relies on ES Modules (import/export), which align with the standard JavaScript module system and bundlers (Webpack/Vite). Namespaces (formerly 'Internal Modules') are a TypeScript-specific construct to organize code globally. They are now considered legacy for most web development because they don't tree-shake well and are harder to integrate with modern module loaders.
21. What is the role of Source Maps when debugging a TypeScript application?
Since browsers and Node.js execute the compiled JavaScript, not the TypeScript source, debugging can be difficult. Source Maps (.map files) create a mapping between the executed JavaScript code and the original TypeScript source lines. This allows developers to set breakpoints and inspect variables directly in their .ts files within Chrome DevTools or VS Code, essentially hiding the compilation step.
22. How does 'Contextual Typing' apply to anonymous functions in TypeScript?
Anonymous functions (e.g., callbacks passed to map or event listeners) often don't require explicit type annotations because TypeScript uses 'Contextual Typing'. The compiler infers the parameter types based on the context in which the function is called. For instance, in numbers.map(n => n * 2), TypeScript knows n is a number because numbers is a number[]. Explicitly typing these is usually redundant 'noise'.
23. What are the two distinct uses of the typeof operator in TypeScript?
In a value position, typeof is the standard JavaScript operator that returns a runtime string (e.g., typeof x === 'string'). In a type position, TypeScript uses typeof to extract the static type of a variable or object (e.g., type User = typeof userObj). This is powerful for keeping types in sync with data constants without duplication.
24. How does TypeScript support OOP principles like Encapsulation and Polymorphism?
TypeScript supports Encapsulation via access modifiers (private, protected, public) and accessors (getters/setters). Polymorphism is achieved through Interfaces and Abstract Classes, allowing different classes to implement the same contract. While TS enforces these at compile time, developers must remember that runtime JavaScript is prototypal, so private fields are technically accessible unless using ES Private Fields (#field).
25. What are the different ways to achieve immutability in TypeScript, and how does readonly differ from const?
const prevents variable reassignment but does not make objects immutable (properties can still change). readonly is a property modifier that prevents modification of a specific property. Readonly<T> is a utility type that makes all properties of an object immutable. For true runtime immutability, Object.freeze() is required, as readonly is stripped during compilation.
26. How are modules classified in TypeScript, and what is the distinction between 'Internal' and 'External' modules?
Historically, TypeScript classified modules as 'Internal' (now called Namespaces) and 'External' (now simply Modules). Internal modules use the namespace keyword to group code within the global scope, useful for legacy browser scripts. External modules use import/export and map one-to-one with files, relying on a module loader (CommonJS/ESM). Modern development almost exclusively uses External Modules.
27. How can you programmatically generate a Type Definition (.d.ts) file from a TypeScript file?
You can generate declaration files using the TypeScript compiler CLI command tsc --declaration (or tsc -d). Alternatively, you can enable the "declaration": true setting in tsconfig.json. This process strips out all implementation logic and leaves only the type signatures, which is a critical step when publishing TypeScript libraries to npm so they can be consumed by other developers.
28. Beyond the basic ?, how can you manipulate Optional Properties using Utility Types?
While ? defines an optional property in an interface, senior developers use Utility Types to manipulate this trait dynamically. Partial<T> makes all properties of a type optional (useful for patch updates), while Required<T> forces all optional properties to become required (useful for validating config objects after defaults are applied). This avoids duplicating interfaces just to change optionality.
29. Explain the concept of Generics in TypeScript. How do they promote code reusability without sacrificing type safety?
Generics allow a component (function, class, or interface) to work over a variety of types rather than a single one, while preserving the type information. Instead of using any (which loses type details), Generics (e.g., <T>) capture the type passed in so it can be used for return types or validation. They are essential for writing reusable utility functions and higher-order components.
30. What are Decorators in TypeScript, and what are their primary practical applications in framework development?
Decorators are a meta-programming feature that allows annotation and modification of classes and members at design time. They are functions that wrap the original code. In frameworks like Angular or NestJS, they are fundamental for Dependency Injection, defining metadata (e.g., @Component, @Get), and implementing Cross-Cutting Concerns like logging or validation without polluting business logic.
31. Explain 'Type Compatibility' in TypeScript. What is structural typing?
TypeScript uses Structural Typing (Duck Typing), meaning two types are compatible if their internal structure (members) is compatible, regardless of their names. If Type A has all the properties of Type B, A can be assigned to B. This contrasts with Nominal Typing (Java/C#), where matches are based on explicit declarations/names. This makes TypeScript flexible for working with anonymous objects and JSON.
32. What are Type Guards, and what is the difference between standard checks and User-Defined Type Guards?
Type Guards are expressions that perform a runtime check that guarantees the type in a scope. Standard guards use typeof or instanceof. User-Defined Type Guards are functions with a return signature of arg is Type (e.g., function isFish(pet): pet is Fish). These are crucial for validating complex interfaces where standard JS checks aren't sufficient.
33. How does TypeScript handle 'this' scoping, and how can you type 'this' in a function?
In JavaScript, this is determined by how a function is called, often leading to errors. TypeScript allows you to annotate the expected type of this as a fake first parameter (e.g., function fn(this: User, ...) { ... }). This ensures the compiler catches invalid calls where the context isn't what the function expects, preventing runtime 'undefined' context errors.
34. How does TypeScript integration with Webpack (or other bundlers) typically work?
TypeScript itself is not a bundler; it is a compiler. Integration usually involves a loader (like ts-loader or babel-loader with a TS preset). The loader intercepts .ts files, feeds them to the TypeScript compiler for type-checking and transpiration to JS, and passes the result back to Webpack for bundling. Modern setups often separate type checking (fork-ts-checker-webpack-plugin) from transpilation (using esbuild/swc) for speed.
35. What is a Namespace in TypeScript, and why has its usage declined in favor of ES Modules?
Namespaces (internal modules) were TypeScript's early solution for organizing code and preventing global scope pollution in the browser (using IIFEs). However, with the standardization of ES Modules (import/export), Namespaces are now largely obsolete for application code. They are primarily used today only for merging declarations or typing legacy libraries that depend on global variables.
36. What are Type Aliases and Interfaces, and what are the specific technical reasons to choose one over the other in modern TypeScript?
Both define shapes. However, Interfaces are restricted to object shapes and support 'Declaration Merging' (automatically combining multiple declarations of the same name), making them ideal for extending 3rd-party libraries. Type Aliases are more flexible, supporting primitives, unions, intersections, and tuples. In modern TS, performance is similar, but Type Aliases are generally preferred for internal component props and state types, while Interfaces are preferred for public API definitions.
37. Explain Discriminated Unions. How do they enable safe polymorphism in TypeScript?
A Discriminated Union is a union of object types that all share a common singleton property (the 'discriminant' or 'tag'), usually a string literal. This allows TypeScript to automatically narrow the type within a switch or if block based on that tag. It is the standard pattern for handling state management actions (like Redux) or disparate API responses without using unsafe type assertions.
38. How does Function/Method Overloading work in TypeScript, and how does the implementation signature differ from the overload signatures?
TypeScript allows defining multiple function signatures (overloads) for a single function name to handle different parameter combinations. However, there must be only one implementation signature that is compatible with all overloads. Crucially, the implementation signature itself is not visible to the caller; only the specific overloads are exposed. This simulates polymorphism while keeping the runtime JavaScript as a single function.
39. What is the keyof type operator and in what scenarios is it critical for type-safe property access?
keyof T produces a union of string/number literal types representing the public keys of T. It is critical for functions that accept an object and a property name (e.g., getProperty(obj, key)). By typing the key as keyof T rather than string, TypeScript ensures you can only request properties that actually exist on the object, preventing runtime 'undefined' access errors.
40. Explain the purpose of Utility Types like Partial<T>, Record<K, T>, and Pick<T, K>. Why should a developer use them?
These built-in mapped types transform existing types. Partial marks all properties as optional (useful for patch updates). Record creates an object type with specific keys and value types (safer than index signatures). Pick creates a subset of a type. Using them reduces code duplication and ensures that derived types (like form inputs) break automatically if the original model changes, alerting developers to update usage.
41. How does TypeScript handling of errors in catch blocks differ in recent versions (v4.0+), and what is the best practice?
Historically, the error variable in a catch(e) clause was any. In modern TypeScript, strict mode (or useUnknownInCatchVariables) treats it as unknown. This forces the developer to check the error type (e.g., if (e instanceof Error)) before accessing properties like .message. This prevents crashes when non-Error objects (like strings or null) are thrown.
42. What is the declare keyword, and how is it used in Ambient Declarations?
The declare keyword tells the compiler "this variable/function exists globally (e.g., added by a script tag or environment), so don't emit code for it, just trust me." It is used for Ambient Declarations, typically in .d.ts files, to type global variables (like window, document, or generic libraries like jQuery) without providing the implementation.
43. What are Index Types (Index Signatures) and what are the risks associated with using them?
Index signatures (e.g., { [key: string]: string }) describe objects where the specific property names are not known, but the value types are. The risk is that TypeScript assumes any string key returns the specified value type, potentially masking undefined returns if a key is missing. Senior developers often prefer Record<string, T> or Map<K, V> for safer dynamic dictionaries.
44. How do ReadonlyArray and as const assertions contribute to immutable data patterns?
ReadonlyArray<T> removes methods that mutate the array (push, pop, splice). as const is a 'const assertion' that makes the compiler infer the narrowest possible type (literals) and marks all properties as readonly recursively. Together, they are powerful tools for Redux reducers or config files where data must remain constant throughout the app lifecycle.
45. How do you handle external third-party libraries that do not provide TypeScript definitions?
First, check strictly typed repositories like DefinitelyTyped (@types/library-name). If none exist, you must create a declaration file (.d.ts). A quick fix is creating a module declaration: declare module 'library-name'; (which types imports as any). A better senior approach is to write a partial declaration typing only the functions you actually use.
46. What is a 'Generic Class', and how does it differ from a standard class?
A Generic Class accepts a type parameter (e.g., class Queue<T> { ... }) allowing it to manage different data types while maintaining type safety for that specific instance. For example, new Queue<number>() will enforce that only numbers are pushed/popped. This is fundamental for creating reusable data structures (Stacks, Queues, Linked Lists) that are type-agnostic until instantiation.
47. How do you effectively type Custom Hooks in React to ensure the returned values are correctly inferred as Tuples or Objects?
When a custom hook returns an array (like useState), TypeScript infers it as a union array (string | number)[] by default. To strictly type it as a tuple (so index 0 is always string, index 1 is function), you must use as const on the return statement (return [value, setValue] as const). This locks the positions and types, allowing safe destructuring in the component.
48. In React with TypeScript, why is React.FC (Function Component) often discouraged in favor of explicitly typing function arguments?
Historically, React.FC implicitly included children, which wasn't always desirable (components that shouldn't accept children would silently accept them). Although React 18 removed implicit children, React.FC still breaks Generic Components in some edge cases and adds unnecessary verbose syntax. The industry standard is now explicitly typing props: const MyComp = ({ name }: Props) => JSX.Element.
49. What is 'Global Augmentation' and how do you correctly add properties to the global window object in TypeScript?
You cannot simply assign window.myVar = 10 because TS doesn't know myVar exists on Window. To fix this, you must use Global Augmentation. You create a .d.ts file (or a module block) containing declare global { interface Window { myVar: number; } }. Because Interfaces support declaration merging, TypeScript merges your custom property into the existing global definition.
50. How do you utilize 'Template Literal Types' to type standard CSS strings or event names?
Template Literal Types allow validation of string patterns. For CSS, you can define a type like type Margin = margin-$0``, which matches 'margin-top' but rejects 'margin-center'. This is powerful for styling libraries or component props to ensure developers only pass valid CSS classes or style keys defined by your design system.
51. Analyze the distinctions between the any, unknown, and never types in TypeScript. When should each be used?
any effectively disables the type checker for a variable; it is an escape hatch that should be avoided unless migrating legacy code. unknown is the type-safe counterpart to any; it represents a value that could be anything, but TypeScript forces you to perform type narrowing (checks) before performing operations on it. never represents a state that should not exist, such as the return type of a function that always throws an exception or an infinite loop, or used in exhaustive switch checks to ensure all cases in a discriminated union are handled.
52. How does TypeScript distinguish between Interfaces and Type Aliases, and what are the best practices for choosing between them?
Functionally, both can define the shape of an object or function. The critical distinction is that Interfaces support 'Declaration Merging' (defining an interface twice merges the definitions), making them ideal for defining public APIs or extending third-party libraries. Type Aliases are more flexible for defining Union types, Intersection types, tuples, and complex utility types. Best practice suggests using Interfaces for defining object contracts (OOP/API shapes) and Types for compositional data structures and unions.
53. Explain the concept of Structural Typing in TypeScript and how it differs from Nominal Typing.
TypeScript uses Structural Typing (often called 'duck typing'), meaning type compatibility is determined by the shape of the data, not its name or declaration. If object A has all the properties required by type B, TypeScript accepts A as B, even if A wasn't explicitly declared as B. This differs from Nominal Typing (used in Java/C#), where explicit inheritance or implementation is required. This design choice aligns TS with JavaScript's dynamic nature but requires developers to be aware of 'excess property checks' in object literals.
54. What are Generics in TypeScript, and how do they contribute to creating reusable, type-safe components?
Generics allow code (functions, classes, interfaces) to work with a variety of types while retaining the relationship between those types. Instead of using any, which loses type information, Generics use a type variable (e.g., <T>) to capture the type provided at the time of use. This is essential for creating reusable components like API wrappers, collections, or Higher-Order Components (HOCs) where the input type dictates the return type, ensuring type safety flows through the application.
55. What is Type Narrowing, and how do user-defined Type Guards work?
Type Narrowing is the process where TypeScript refines a broad type (like unknown or a union) into a more specific one based on control flow analysis (e.g., using if statements with typeof). User-defined Type Guards are functions with a return type predicate of the form arg is Type (e.g., isUser(u: any): u is User). If the function returns true, TypeScript instructs the compiler to treat the variable as that specific type in the subsequent scope.
56. Explain the utility of Discriminated Unions and how they facilitate type-safe state management.
Discriminated Unions (or Tagged Unions) involve combining multiple object types that all share a common literal property (the 'discriminant' or 'tag'), such as { kind: 'loading' } | { kind: 'success', data: T }. TypeScript can use this tag in control flow analysis (switch/if) to automatically narrow the union to the specific member. This is a pattern of choice for handling application state (Redux actions, API responses) as it makes invalid states unrepresentable.
57. How do you handle third-party JavaScript libraries that lack TypeScript definitions?
First, check for a community-maintained definition package in the DefinitelyTyped repository (e.g., npm install @types/library-name). If none exists, create a declaration file (.d.ts) in the project. A basic solution is adding declare module 'library-name'; which types the library as any. A senior approach involves writing a custom type definition describing only the specific methods used in the project, ensuring safety for the consumed parts of the library.
58. What is the role of tsconfig.json, and how do target, lib, and module settings affect the build?
tsconfig.json is the root configuration defining the compilation context. target determines the version of ECMAScript syntax the code compiles down to (e.g., ES5 for older browsers, ESNext for modern Node). lib tells TS which environment definitions to include (e.g., 'DOM' for browsers, 'ES2020' for array methods). module determines the module system used in the output code (e.g., CommonJS for Node, ESNext for bundlers like Webpack/Vite). Misconfiguring these often leads to runtime errors or failed builds.
59. Compare TypeScript Enums with Union Types and Objects. Why might a senior developer prefer Union Types over Enums?
Enums are one of the few TypeScript features that emit runtime code (an IIFE for mapping). Numeric enums are not type-safe (allowing out-of-bounds access in older versions), and String enums can increase bundle size due to lack of tree-shaking support in some bundlers. Union types of string literals (e.g., type Status = 'open' | 'closed') are entirely erased at compile time, offer the same autocomplete benefits, and are generally preferred for keeping the bundle size minimal and the syntax aligned with standard JavaScript patterns.
60. What are Index Signatures, and when should they be used versus the Record<K, T> utility type?
Index Signatures (e.g., [key: string]: number) describe objects where the specific property names are not known ahead of time but the shape of the values is consistent. They are essential for dynamic data structures like caches or configuration dictionaries. Record<K, T> is a cleaner, more readable shorthand for the same concept when the key type K is a union of literals or string, making it generally preferred for mapping known keys to values.
61. How does Function Overloading work in TypeScript, and how does it differ from overloading in languages like Java or C#?
In languages like Java, overloading implies multiple function implementations with different signatures. In TypeScript, overloading is a compile-time feature only. You define multiple 'call signatures' (declarations) followed by a single 'implementation signature' that must be compatible with all declarations. The implementation signature is not exposed to the caller. This allows a single JavaScript function to robustly handle varying argument shapes while providing accurate type suggestions for each usage scenario.
62. How do you type this in TypeScript functions to prevent context-related runtime errors?
TypeScript allows you to declare this as a fake first parameter in a function signature (e.g., function handling(this: User) { ... }). This is erased during compilation but ensures that the function is called with the correct context. If strict compile options are on (noImplicitThis), TypeScript will error if this is inferred as any. This is particularly useful when defining callbacks for libraries that manipulate context, or when using standard functions in class methods instead of arrow functions.
63. What is the distinction between Abstract Classes and Interfaces in TypeScript architecture?
Interfaces are zero-runtime artifacts used purely for defining contracts (shapes). Abstract Classes are valid JavaScript classes that can contain implementation details (runtime code) alongside abstract methods (contracts). Use Abstract Classes when you need to share implementation logic across subclasses (like a base Service class with a shared logger) but prevent direct instantiation. Use Interfaces when you only need to enforce a specific structure across disparate classes.
64. Differentiate between TypeScript's private/protected modifiers and JavaScript's native private fields (#).
TypeScript's private and protected are 'soft private'; they enforce encapsulation only during compilation. At runtime, the transpiled code is standard JavaScript, meaning these properties are accessible if one bypasses the type checker. JavaScript's native private fields (prefixed with #) provide 'hard privacy' enforced by the JavaScript engine itself, making the data truly inaccessible from outside the class at runtime. Senior developers choose # for true security and TS private for API guidance.
65. What are the best practices for using TypeScript with React, specifically regarding React.FC vs. explicit props?
Historically, React.FC was common as it implicitly included the children prop. However, modern best practice leans toward defining components as standard functions with explicit Prop interfaces. This prevents issues where components unintentionally accept children and improves default prop handling. Additionally, generic components (e.g., <List<T> items={...} />) are easier to type using standard function syntax rather than React.FC.
66. How do you configure a Node.js project with TypeScript to handle development execution versus production builds?
For development, ts-node (or faster alternatives like tsx or swc) is used to execute TypeScript files directly in memory without emitting files. For production, the standard workflow is to compile code using tsc to a dist/ or build/ folder (targeting a specific Node version, usually CommonJS or ESModules) and run that output with node. It is critical to ensure tsconfig.json excludes test files and development scripts to keep the production artifact clean.
67. How is ESLint configured for TypeScript, and why is TSLint deprecated?
TSLint was deprecated in favor of typescript-eslint, which allows ESLint (the standard JS linter) to parse TypeScript code. This unification permits using the vast ecosystem of ESLint rules (like React hooks or accessibility checks) alongside type-aware rules. Configuration involves installing the parser and plugin (@typescript-eslint/parser, @typescript-eslint/eslint-plugin) and extending recommended rule sets. This setup is superior because it checks both code quality and type correctness in a single pass.
68. What are Intersection Types (&), and how do they function differently from Interface extension?
Intersection Types combine multiple types into one (e.g., type C = A & B), requiring the resulting object to have properties of both A and B. Unlike Interface extension (interface C extends A, B), which can error if A and B have conflicting property definitions (e.g., id is string in A but number in B), an Intersection will attempt to merge them (resulting in id: string & number, which resolves to never). Intersections are preferred for composing ad-hoc types or mixins.
69. What is the specific impact of the strict compiler option in tsconfig.json?
The strict flag is a meta-setting that enables a suite of strict type-checking options simultaneously. It turns on noImplicitAny (disallowing inference of 'any'), strictNullChecks (handling null/undefined), strictFunctionTypes (checking function argument variance), strictBindCallApply, and strictPropertyInitialization (ensuring class properties are set in the constructor). Enabling this is the industry standard for new projects to ensure the highest level of type safety.
70. When configuring TypeScript with bundlers like Webpack or Vite, what is the concept of 'Type Stripping' vs. 'Type Checking'?
Modern bundlers like Vite (using esbuild) or Babel typically perform 'Type Stripping' or 'Erasure'. They strip out TypeScript annotations to generate valid JavaScript very quickly but do not check for type errors. This means a build can succeed even with type violations. To ensure safety, senior developers must run the TypeScript compiler (tsc --noEmit) as a parallel process or a pre-commit hook to perform actual 'Type Checking' before the bundler generates the production assets.
71. How does TypeScript's support for OOP principles differ from Nominal Typing languages like Java?
While TypeScript supports OOP features like classes, inheritance, and access modifiers (private, public), it remains a Structurally Typed language. In Java (Nominal), two classes with the same properties are distinct types. In TypeScript, if Class A and Class B have the same public shape, they are interchangeable. This means TS OOP is more flexible but requires developers to be careful when relying on instanceof checks or expecting strict type isolation based solely on class names.
72. How can you derive a new type that represents a specific subset of properties from an existing Interface?
You should use the Pick and Omit utility types to create subsets without code duplication.
-
Pick<T, K>: Constructs a type by picking the set of propertiesKfromT.type UserPreview = Pick<User, 'id' | 'name'>; -
Omit<T, K>: Constructs a type by picking all properties fromTand then removingK.type NewUser = Omit<User, 'id'>;
Senior Note: Avoid manually redefining properties. Using utility types ensures that if the source interface changes (e.g., a field type is updated), the subset type updates automatically, maintaining a single source of truth.
73. Explain how Enums function in TypeScript. What are the potential pitfalls regarding bundle size and tree-shaking?
Enums allow definition of a set of named constants (numeric or string). Unlike interfaces, Enums define valid JavaScript code at runtime (usually an IIFE generating an object).
Pitfalls & Best Practices: Standard enums can increase bundle size because they are not always tree-shakable by bundlers (like Webpack or Rollup).
For optimization, prefer:
- Const Enums (
const enum): These are erased during compilation and values are inlined. - Union Types or Objects as const:
const Direction = { Up: 'UP', Down: 'DOWN' } as const. This pattern is standard JavaScript, completely tree-shakable, and often preferred in modern TS development.
74. What is the specific use case for the never type, and how does it distinctively differ from void?
Void indicates a function returns nothing (technically returns undefined in JS). It finishes execution normally.
Never indicates a value that will never occur. A function returning never typically:
- Throws an exception.
- Has an infinite loop.
Senior Use Case: never is critical for Exhaustive Type Checking in switch statements.
Example:
function assertUnreachable(x: never): never { throw new Error(...) }
If you add a case to a Union type but forget to handle it in a switch, TypeScript will throw an error because it tries to pass the unhandled type to assertUnreachable, which expects never.
75. Explain Generics in TypeScript. How do constraints (extends) improve the safety of generic functions?
Generics allow you to create reusable components/functions that work over a variety of types rather than a single one, while preserving type safety (avoiding any).
Constraints:
Using extends restricts the generic type T.
Example: function getLength<T extends { length: number }>(arg: T) { return arg.length; }
Without the constraint, TypeScript wouldn't know that arg has a .length property. This ensures the function is reusable but only for types that satisfy the contract, preventing runtime errors.
76. In what scenario is the unknown type preferred over any, and how do you safely utilize variables typed as unknown?
unknown is the type-safe counterpart to any.
Difference:
any: Allows you to do anything (access properties, call functions) without checks. It effectively disables TypeScript.unknown: Allows you to assign any value to it, but you cannot perform operations on it until you narrow the type.
Usage: Use unknown for external input (API responses, parsing JSON) where the shape is not guaranteed. You safely use it by performing Type Narrowing (using typeof, instanceof, or custom type guards) before accessing properties.
77. What is the purpose of the noImplicitAny compiler flag, and why is it considered a standard for strict TypeScript configurations?
noImplicitAny raises a compiler error whenever a variable or parameter has an inferred type of any.
Reasoning:
If TypeScript cannot infer a type, it defaults to any, silently losing type safety. Enabling this flag forces the developer to explicitly type arguments or variables that cannot be inferred.
Senior Context: This is the first step in migrating a project to strict TypeScript. It prevents "lazy" coding where developers forget to define contracts, ensuring the codebase doesn't degrade into standard JavaScript disguised as TypeScript.
78. Differentiate between Union Types and Intersection Types. Provide a practical example where an Intersection Type is required.
- Union (
|): Represents a value that can be one of several types. Example:string | number. - Intersection (
&): Represents a value that combines all features of multiple types.
Intersection Use Case:
Composing objects or mixins.
type Draggable = { drag: () => void };
type Resizable = { resize: () => void };
type UIComponent = Draggable & Resizable;
A UIComponent must have both the drag and resize methods. This is frequently used when modeling API responses that combine base entity fields (id, timestamps) with specific data fields.
79. What is the semantic difference between extends and implements when defining Classes and Interfaces?
- extends (Inheritance): Used by a class to inherit from another class, or an interface to inherit from another interface. It implies an "is-a" relationship and, in the case of classes, inherits code/implementation.
- implements (Contract): Used by a class to adhere to an interface. It implies a "satisfies" relationship. The class must define all properties and methods declared in the interface, but it inherits no implementation code.
Note: A class can extend only one parent class, but it can implement multiple interfaces.
80. What role do Abstract Classes play in system design, and how do they differ from regular classes and interfaces?
Abstract classes serve as base classes that cannot be instantiated directly. They allow you to define implementation details for some methods while forcing derived classes to implement specific abstract methods.
Vs Interfaces: Interfaces describe shape but contain no code. Abstract classes can contain shared logic (e.g., a logging method or constructor logic) that subclasses inherit.
Use Case: Building a framework where you provide a generic BaseService with error handling logic, but require the developer to implement the specific fetchData() method.
81. Explain TypeScript Decorators and provide a use case for where they are most effective.
Decorators are a special kind of declaration that can be attached to a class, method, accessor, property, or parameter. They are essentially higher-order functions that allow you to modify the behavior of the target at design time (meta-programming). A common use case is in frameworks like NestJS or Angular, where decorators like @Controller or @Injectable represent metadata injection or aspect-oriented programming patterns (e.g., logging or validation) without cluttering the business logic.
82. Analyze how TypeScript manages null and undefined, specifically in the context of the --strictNullChecks flag.
By default, null and undefined are subtypes of all other types. However, this is a common source of runtime errors. When strict null checking (--strictNullChecks) is enabled—which is highly recommended—null and undefined are treated as distinct types. This forces the developer to explicitly handle these cases (e.g., via Union types like string | null) and perform checks before accessing properties, eliminating a vast class of 'undefined is not a function' errors.
83. What are Generics in TypeScript, and how do they contribute to creating reusable, type-safe components?
Generics provide a way to create components that can work over a variety of types rather than a single one, while still preserving the relationship between the input and output types. For example, function identity<T>(arg: T): T captures the type passed in so it can be asserted on the return value. This is critical for building reusable utilities, data structures, and HOCs (Higher Order Components) that remain type-safe regardless of the data usage context.
84. What are Namespaces in TypeScript, and how do they differ from standard ES Modules?
Namespaces (formerly 'internal modules') are a TypeScript-specific way to organize code using IIFE (Immediately Invoked Function Expression) patterns to group related functionalities and avoid global scope pollution. However, in modern TypeScript development, standard ES Modules (import/export) are preferred for their compatibility with bundlers and the wider JS ecosystem. Namespaces are now primarily used for organizing complex .d.ts type definition files.
85. How do you integrate TypeScript with third-party libraries that lack built-in type definitions?
When a library (like legacy jQuery or lodash) lacks built-in types, you rely on DefinitelyTyped, a community repository of type definitions. You install them via @types/library-name. If no types exist there, you can create a custom d.ts file in your project (e.g., declare module 'library-name';) to silence compiler errors, or write your own type definitions to gain type safety for the specific methods you are using.
86. What are Type Guards, and how do user-defined type guards assist in type narrowing?
Type guards are expressions that perform a runtime check which guarantees the type in some scope. TypeScript recognizes built-in guards like typeof, instanceof, and in. User-defined type guards are functions that return a type predicate (e.g., function isFish(pet: Fish | Bird): pet is Fish). These are crucial when working with Union types, allowing the compiler to 'narrow' a broad type down to a specific one within a conditional block.
87. Explain the concept of Structural Typing (Type Compatibility) in TypeScript.
TypeScript uses Structural Typing (often called 'duck typing'), meaning that type compatibility is based on the shape of the members, not the name of the type. If Class A and Class B have the exact same properties and methods, TypeScript considers them compatible, even if they don't extend each other. This contrasts with Nominal Typing (used in Java/C#), where the explicit class hierarchy defines compatibility. This flexibility aligns TypeScript more closely with JavaScript's dynamic nature.
88. Compare and contrast type and interface. When should you choose one over the other?
Both define shapes. Interfaces are restricted to object types and support Declaration Merging (adding fields to an interface by re-declaring it), making them ideal for libraries and extending global scopes. Types are more flexible, capable of defining primitive aliases, unions, and tuples. In modern TypeScript, the performance difference is negligible. The general rule of thumb: Use interface for public API definitions and object shapes; use type for unions, intersections, and complex utility types.
89. Describe Discriminated Unions (Tagging) and how they facilitate type-safe exhaustive checking.
A Discriminated Union involves combining types that share a common literal property (the 'discriminant' or 'tag'), usually named kind or type. TypeScript can narrow the type of the union based on this tag in a switch statement or conditional. This pattern is essential for reducing complex state logic into predictable flows. It pairs well with the never type to ensure 'exhaustive checking'—ensuring that all possible cases of the union are handled in the code.
90. How is Function/Method Overloading achieved in TypeScript compared to languages like C# or Java?
In TypeScript, overloading is a compile-time concept. You define multiple 'overload signatures' (the declarations) followed by a single 'implementation signature'. The implementation signature must be compatible with all overload signatures but is not directly callable. This differs from Java/C#, where you effectively have multiple different functions. In TypeScript, the resulting JavaScript is just one function that must manually parse the arguments to determine how to behave.
91. Explain the keyof type operator and its utility in creating dynamic, type-safe property accessors.
The keyof operator takes an object type and produces a string or numeric literal union of its keys. For example, if interface Person { name: string; age: number }, then keyof Person is 'name' | 'age'. This is crucial for generic functions like getProperty<T, K extends keyof T>(obj: T, key: K), ensuring that you cannot request a property key that doesn't exist on the object.
92. Demonstrate the use of built-in Utility Types: Partial<T>, Readonly<T>, and Record<K, T>.
Partial<T> constructs a type with all properties of T set to optional. Readonly<T> constructs a type with all properties of T set to readonly (immutable). Record<K, T> constructs an object type whose property keys are K and whose property values are T. These utilities are indispensable for DTO transformations, state management (where updates might be partial), and mapping dictionaries without manually redefining interfaces.
93. What is the recommended approach for handling errors in TypeScript given that catch clauses default to any or unknown?
In TypeScript versions 4.0+, the catch clause variable can be typed as unknown (and defaults to it if useUnknownInCatchVariables is on). This is safer than any. The best practice is to use a Type Guard within the catch block (e.g., if (error instanceof Error)) to narrow the type before accessing error.message. This accounts for the fact that JavaScript can throw anything (strings, numbers, objects), not just Error instances.
94. Demonstrate the syntax and constraints for implementing a Class that utilizes Generics.
A generic class uses type parameters (e.g., <T>) declared after the class name. class Box<T> { contents: T; constructor(val: T) { this.contents = val; } }. You can enforce constraints using extends, e.g., class DataManager<T extends { id: string }>. This ensures that whatever type is passed into the class has at least the properties defined in the constraint, allowing the class implementation to access those properties safely.
95. How does TypeScript facilitate Dynamic Imports (Code Splitting)?
TypeScript supports the import() expression, which returns a Promise of the module. This allows for lazy loading modules only when needed. TypeScript types the resolution of the promise based on the module's exports. This is essential for modern frontend performance (Code Splitting). To use this, the module compiler option in tsconfig.json must be set to esnext, commonjs, or similar formats that support deferred loading.
96. Explain Index Signatures. How do you type an object with dynamic keys?
Index signatures are used when the names of object properties are not known ahead of time, but the type of the values is. Syntax: interface StringMap { [key: string]: string; }. A critical limitation is that if an index signature is defined, all specific named properties in that type must be compatible with the index signature's type (e.g., you cannot have a count: number property if the index signature says all values return string).
97. What mechanisms does TypeScript provide to enforce immutability on Arrays and Object properties?
TypeScript provides the readonly modifier for object properties and the ReadonlyArray<T> type (or readonly T[]) for arrays. These prevent assignment to the property or mutation methods (push, pop) on the array. For deeply nested immutability or literal types, 'const assertions' (as const) can be used, which makes the object and its properties readonly and narrows literals to their exact values rather than general types like string.
98. What is the specific purpose of .d.ts files, and how does the compiler utilize them?
.d.ts files (Declaration Files) contain only type information and no implementation logic. They act as a bridge between the TypeScript compiler and JavaScript code. The compiler uses them to perform type checking and provide IntelliSense for libraries that are written in JavaScript or compiled to JavaScript. They are effectively the 'header files' of the TypeScript ecosystem.
99. Explain the limitations and implementation details of Function Overloading in TypeScript.
Function overloading allows defining multiple signatures for a single function, but it has limitations compared to other languages. You must define all public signatures first, followed by one single implementation signature. This implementation signature is hidden from the public API and must be broad enough to handle the inputs of all overload signatures. At runtime, the code must essentially inspect arguments (type guards) to decide which logic path to execute.
100. What are Intersection Types (&), and how do they differ from Union Types?
Intersection types (A & B) combine multiple types into one. An object of this type must satisfy the contracts of both A and B simultaneously. This is commonly used for mixins or combining configuration objects. This differs from Union types (A | B), where the object only needs to satisfy one of the types. If two types in an intersection share a property with incompatible types (e.g., string vs number), that property resolves to never.
Last updated on
Spotted something unclear or wrong on this page?