TypeScript Interview Questions

43 questions with detailed answers

Question:
What is TypeScript and how does it differ from JavaScript?
Answer:
TypeScript is a statically typed superset of JavaScript developed by Microsoft. Key differences: 1) Static typing - TypeScript adds optional static type checking, 2) Compile-time error detection - catches errors before runtime, 3) Better IDE support - enhanced IntelliSense and refactoring, 4) Modern JavaScript features - supports latest ECMAScript features, 5) Compilation - TypeScript compiles to plain JavaScript.

Question:
What are the main benefits of using TypeScript over JavaScript?
Answer:
Static typing, better IDE support, early error detection, improved code maintainability, enhanced refactoring capabilities, better documentation through types.

Question:
Explain the difference between interface and type in TypeScript.
Answer:
Interfaces are extensible and can be merged, primarily for object shapes. Types are more flexible, support unions/intersections, but cannot be extended or merged.

Question:
How do you set up a basic TypeScript project?
Answer:
Install TypeScript, create tsconfig.json, write .ts files, compile with tsc command, run the generated JavaScript files.

Question:
What is TypeScript and why would you use it?
Answer:
TypeScript is a statically typed superset of JavaScript developed by Microsoft. • Static Typing: Provides compile-time type checking • Better IDE Support: Enhanced autocomplete, refactoring, and navigation • Early Error Detection: Catches errors during development • Improved Code Quality: Makes code more maintainable and readable • Modern JavaScript Features: Supports latest ECMAScript features // Example: Type safety function greet(name: string): string { return 'Hello, ' + name + '!'; } greet(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string' Benefits: • Reduces runtime errors • Improves team collaboration • Better documentation through types • Easier refactoring of large codebases.

Question:
Explain the basic types in TypeScript.
Answer:
TypeScript provides several built-in types for type safety. Primitive Types: • string: Text data • number: Numeric values (integers and floats) • boolean: True/false values • null: Intentional absence of value • undefined: Variable declared but not assigned let name: string = "John"; let age: number = 25; let isActive: boolean = true; let data: null = null; let value: undefined = undefined; Special Types: • any: Disables type checking • void: No return value (functions) • never: Values that never occur • unknown: Type-safe alternative to any function logMessage(): void { console.log("Hello"); } function throwError(): never { throw new Error("Something went wrong"); }

Question:
How do you define and use interfaces in TypeScript?
Answer:
Interfaces define the structure of objects and provide contracts for classes. Basic Interface: interface User { id: number; name: string; email?: string; // Optional property readonly createdAt: Date; // Read-only property } const user: User = { id: 1, name: "John Doe", createdAt: new Date() }; Interface Features: • Optional Properties: Use `?` for optional fields • Readonly Properties: Use `readonly` to prevent modification • Method Signatures: Define function types interface Calculator { add(a: number, b: number): number; subtract(a: number, b: number): number; } class BasicCalculator implements Calculator { add(a: number, b: number): number { return a + b; } subtract(a: number, b: number): number { return a - b; } } Interface Extension: interface Animal { name: string; } interface Dog extends Animal { breed: string; }

Question:
Explain the different types available in TypeScript.
Answer:
TypeScript provides several built-in types: 1) Primitive types: number, string, boolean, null, undefined, symbol, bigint, 2) Object types: object, array, function, 3) Special types: any (disables type checking), unknown (type-safe any), void (no return value), never (never returns), 4) Union types: string | number, 5) Intersection types: Type1 & Type2, 6) Literal types: specific string/number values, 7) Custom types: interfaces, type aliases, enums, classes.

Question:
What are interfaces in TypeScript and how do you use them?
Answer:
Interfaces define the structure of objects, specifying what properties and methods an object should have. Example: interface User { name: string; age: number; email?: string; } - Optional properties use ?, readonly properties prevent modification, interfaces can extend other interfaces, and they support method signatures. Interfaces are purely for compile-time type checking and are removed during compilation.

Question:
Explain generics in TypeScript with examples.
Answer:
Generics allow creating reusable components that work with multiple types while maintaining type safety. Basic Generic Function: function identity(arg: T): T { return arg; } let output = identity('Hello'); let numberOutput = identity(42); Generic Interface: interface GenericIdentityFn { (arg: T): T; } function identity(arg: T): T { return arg; } let myIdentity: GenericIdentityFn = identity; Generic Classes: class GenericNumber { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; }; Benefits: • Type safety without losing flexibility • Code reusability across different types • Better IntelliSense and error detection • Compile-time type checking.

Question:
What are utility types in TypeScript? Provide examples.
Answer:
Utility types are built-in type transformations that help manipulate existing types. Common Utility Types: • Partial: Makes all properties optional interface User { id: number; name: string; email: string; } type PartialUser = Partial; // All properties optional • Required: Makes all properties required type RequiredUser = Required; // All properties required • Pick: Selects specific properties type UserSummary = Pick; // Only id and name • Omit: Excludes specific properties type UserWithoutEmail = Omit; // Excludes email • Record: Creates object type with specific keys and values type UserRoles = Record; // { [key: string]: boolean } Benefits: • Reduce code duplication • Create variations of existing types • Maintain type safety during transformations.

Question:
How do union and intersection types work in TypeScript?
Answer:
Union and intersection types provide powerful ways to combine types in TypeScript. Union Types (|): Allow a value to be one of several types: type StringOrNumber = string | number; type Status = 'loading' | 'success' | 'error'; function printId(id: string | number) { if (typeof id === 'string') { console.log(id.toUpperCase()); } else { console.log(id.toFixed(2)); } } Intersection Types (&): Combine multiple types into one, requiring all properties: interface Person { name: string; age: number; } interface Employee { employeeId: number; department: string; } type PersonEmployee = Person & Employee; const worker: PersonEmployee = { name: 'John', age: 30, employeeId: 123, department: 'IT' }; Key Differences: • Union: Value can be ANY of the types • Intersection: Value must have ALL properties from combined types • Union narrows with type guards • Intersection creates new composite type.

Question:
Explain type guards and their importance.
Answer:
Type guards narrow types at runtime using typeof, instanceof, or custom predicates. They ensure type safety in conditional blocks.

Question:
What are mapped types and how are they used?
Answer:
Mapped types transform existing types by iterating over properties. They use [P in keyof T] syntax to create new types based on existing ones.

Question:
How do decorators work in TypeScript?
Answer:
Decorators are functions that modify classes, methods, or properties. They use @ syntax and require experimentalDecorators flag. Common in frameworks like Angular.

Question:
Explain the module system in TypeScript.
Answer:
TypeScript supports ES6 modules with import/export. It can compile to different module systems (CommonJS, AMD, UMD) and supports both default and named exports.

Question:
What are conditional types and when would you use them?
Answer:
Conditional types use T extends U ? X : Y syntax to create types based on conditions. Useful for creating flexible APIs and type transformations.

Question:
Explain advanced generic constraints and conditional types.
Answer:
Advanced generics combine constraints, conditional types, and inference for powerful type transformations. Generic Constraints: interface Lengthwise { length: number; } function loggingIdentity(arg: T): T { console.log(arg.length); return arg; } loggingIdentity('hello'); // Works loggingIdentity([1, 2, 3]); // Works loggingIdentity(3); // Error: number does not have length Conditional Types: type NonNullable = T extends null | undefined ? never : T; type ApiResponse = T extends string ? { message: T } : { data: T }; // Usage type StringResponse = ApiResponse; // { message: string } type NumberResponse = ApiResponse; // { data: number } Advanced Pattern with Infer: type ReturnType = T extends (...args: any[]) => infer R ? R : never; type Parameters = T extends (...args: infer P) => any ? P : never; // Extract nested array type type Flatten = T extends (infer U)[] ? U : T; type StringArray = Flatten; // string type NumberType = Flatten; // number Benefits: • Enable complex type transformations • Create flexible, reusable type utilities • Provide compile-time guarantees • Support advanced API design patterns.

Question:
How do you implement type-safe function composition in TypeScript?
Answer:
Type-safe function composition ensures input and output type compatibility. Basic composition function: function compose(f: (b: B) => C, g: (a: A) => B): (a: A) => C { return (a: A) => f(g(a)); } Example usage: const addOne = (x: number) => x + 1; const toString = (x: number) => x.toString(); const composed = compose(toString, addOne); Pipe implementation: function pipe(value: T, ...fns: Function[]): any { return fns.reduce((acc, fn) => fn(acc), value); } const result = pipe(5, addOne, toString); Benefits: Type safety, function reusability, clear data flow.

Question:
Explain template literal types and their use cases.
Answer:
Template literal types create string patterns at the type level for powerful string manipulation. Basic Template Literals: type World = 'world'; type Greeting = \`hello ${World}\`; // 'hello world' type EmailLocaleIDs = 'welcome_email' | 'email_heading'; type FooterLocaleIDs = 'footer_title' | 'footer_sendoff'; type AllLocaleIDs = \`${EmailLocaleIDs | FooterLocaleIDs}_id\`; // 'welcome_email_id' | 'email_heading_id' | 'footer_title_id' | 'footer_sendoff_id' API Route Generation: type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; type Resource = 'users' | 'posts' | 'comments'; type APIRoute = \`/${Resource}\` | \`/${Resource}/${string}\`; type RouteHandler = { [K in HTTPMethod as \`${K} ${T}\`]: (req: Request) => Response; }; // Usage const handlers: Partial> = { 'GET /users': (req) => { /* ... */ }, 'POST /users': (req) => { /* ... */ } }; CSS-in-JS Type Safety: type CSSProperties = { color?: string; backgroundColor?: string; fontSize?: string; }; type StyledComponent = { [K in T as \`${K}Styled\`]: (props: CSSProperties) => JSX.Element; }; // Generate styled components type ButtonComponents = StyledComponent<'primary' | 'secondary'>; // { primaryStyled: ..., secondaryStyled: ... } Benefits: • Type-safe string manipulation at compile time • Generate API routes and handlers • Create CSS-in-JS type safety • Build complex string patterns • Enable advanced metaprogramming.

Question:
How do you create a deep readonly type that works recursively?
Answer:
Deep readonly types recursively apply readonly to all nested properties and arrays. Basic Deep Readonly: type DeepReadonly = { readonly [P in keyof T]: T[P] extends object ? T[P] extends Function ? T[P] : DeepReadonly : T[P]; }; interface User { id: number; profile: { name: string; settings: { theme: string; notifications: boolean; }; }; hobbies: string[]; } type ReadonlyUser = DeepReadonly; // All properties become readonly recursively Advanced Version with Array Handling: type DeepReadonlyAdvanced = T extends (infer R)[] ? ReadonlyArray> : T extends Function ? T : T extends object ? { readonly [K in keyof T]: DeepReadonlyAdvanced } : T; type TestArray = DeepReadonlyAdvanced<{ items: { id: number; tags: string[]; }[]; }>; // { readonly items: ReadonlyArray<{ readonly id: number; readonly tags: ReadonlyArray }> } Practical Implementation: function deepFreeze(obj: T): DeepReadonly { Object.getOwnPropertyNames(obj).forEach(prop => { const value = (obj as any)[prop]; if (value && typeof value === 'object') { deepFreeze(value); } }); return Object.freeze(obj) as DeepReadonly; } const mutableUser: User = { id: 1, profile: { name: 'John', settings: { theme: 'dark', notifications: true } }, hobbies: ['reading', 'coding'] }; const immutableUser = deepFreeze(mutableUser); // immutableUser.profile.name = 'Jane'; // Error: readonly Benefits: • Prevents accidental mutations • Ensures immutability at compile time • Works with nested objects and arrays • Provides runtime immutability when combined with Object.freeze.

Question:
Explain variance in TypeScript and its implications.
Answer:
Variance describes how subtyping relationships are preserved in generic types. Covariance allows subtypes in output positions (function return types). Contravariance allows supertypes in input positions (function parameters). This ensures type safety in function assignments and prevents runtime errors.

Question:
How do you implement a type-safe state machine in TypeScript?
Answer:
Type-safe state machines use discriminated unions to ensure valid state transitions. They prevent invalid state changes at compile time using TypeScript type system.

Question:
Explain the module augmentation pattern in TypeScript.
Answer:
Module augmentation extends existing modules by declaring additional properties or methods. Basic Module Augmentation: // Extending global interfaces declare global { interface Window { myCustomProperty: string; myCustomMethod(): void; } } // Now you can use it window.myCustomProperty = 'Hello'; window.myCustomMethod = () => console.log('Custom method'); Augmenting Third-Party Libraries: // Extending Express Request interface import { Request } from 'express'; declare module 'express-serve-static-core' { interface Request { user?: { id: string; email: string; }; sessionId?: string; } } // Now you can use req.user with type safety app.use((req, res, next) => { req.user = { id: '123', email: 'user@example.com' }; next(); }); Augmenting Node.js Global: // Adding custom properties to Node.js global declare global { namespace NodeJS { interface Global { myAppConfig: { apiUrl: string; version: string; }; } } } // Usage global.myAppConfig = { apiUrl: 'https://api.example.com', version: '1.0.0' }; Augmenting Existing Modules: // Extending Array prototype declare global { interface Array { first(): T | undefined; last(): T | undefined; } } Array.prototype.first = function(this: T[]): T | undefined { return this[0]; }; Array.prototype.last = function(this: T[]): T | undefined { return this[this.length - 1]; }; // Usage with type safety const numbers = [1, 2, 3]; const first = numbers.first(); // Type: number | undefined const last = numbers.last(); // Type: number | undefined Library-Specific Augmentation: // Extending jQuery (if using) declare module 'jquery' { interface JQuery { customPlugin(options?: any): JQuery; } } // Extending Lodash import { LoDashStatic } from 'lodash'; declare module 'lodash' { interface LoDashStatic { customUtility(input: T): T; } } Benefits: • Add type safety to existing libraries • Extend global objects safely • Maintain compatibility with third-party code • Enable custom functionality with type checking.

Question:
How do you create a type that extracts all possible object paths?
Answer:
Object path extraction uses recursive conditional types with template literals to traverse object structure. Basic Path Extraction: type Paths = T extends object ? { [K in keyof T]: K extends string ? T[K] extends object ? K | `${K}.${Paths}` : K : never; }[keyof T] : never; type User = { id: number; profile: { name: string; address: { street: string; city: string; }; }; hobbies: string[]; }; type UserPaths = Paths; // 'id' | 'profile' | 'hobbies' | 'profile.name' | 'profile.address' | 'profile.address.street' | 'profile.address.city' Advanced Path Extraction with Array Handling: type PathsAdvanced = { [K in keyof T]: K extends string ? T[K] extends (infer U)[] ? `${Prefix}${K}` | `${Prefix}${K}.${number}` | PathsAdvanced : T[K] extends object ? `${Prefix}${K}` | PathsAdvanced : `${Prefix}${K}` : never; }[keyof T]; type ComplexObject = { users: { id: number; profile: { name: string; }; }[]; settings: { theme: string; }; }; type ComplexPaths = PathsAdvanced; // 'users' | 'settings' | 'users.0' | 'users.0.id' | 'users.0.profile' | 'users.0.profile.name' | 'settings.theme' Type-Safe Get Function: type PathValue = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue : never : P extends keyof T ? T[P] : never; function get>( obj: T, path: P ): PathValue { const keys = path.split('.'); let result: any = obj; for (const key of keys) { result = result?.[key]; } return result; } // Usage with type safety const user: User = { id: 1, profile: { name: 'John', address: { street: '123 Main St', city: 'New York' } }, hobbies: ['reading'] }; const name = get(user, 'profile.name'); // Type: string const city = get(user, 'profile.address.city'); // Type: string // const invalid = get(user, 'profile.invalid'); // Error! Set Function Implementation: function set>( obj: T, path: P, value: PathValue ): T { const keys = path.split('.'); let current: any = obj; for (let i = 0; i < keys.length - 1; i++) { current = current[keys[i]]; } current[keys[keys.length - 1]] = value; return obj; } // Usage set(user, 'profile.name', 'Jane'); // Type-safe // set(user, 'profile.name', 123); // Error: number not assignable to string Benefits: • Type-safe object property access • Compile-time path validation • Excellent autocomplete support • Prevents runtime property access errors.

Question:
Explain branded types and their use cases in TypeScript.
Answer:
Branded types add nominal typing to structural types using unique symbols or phantom properties. Basic Branded Type: type Brand = T & { __brand: B }; type UserId = Brand; type ProductId = Brand; function createUserId(id: string): UserId { return id as UserId; } function createProductId(id: string): ProductId { return id as ProductId; } // Usage const userId = createUserId('user123'); const productId = createProductId('prod456'); // This prevents mixing different ID types function getUser(id: UserId) { /* ... */ } getUser(userId); // OK // getUser(productId); // Error: ProductId not assignable to UserId Phantom Type Implementation: declare const __brand: unique symbol; type Branded = T & { [__brand]: B }; type Email = Branded; type Password = Branded; function validateEmail(email: string): Email | null { return email.includes('@') ? email as Email : null; } function hashPassword(password: string): Password { return ('hashed_' + password) as Password; } // Type-safe authentication function authenticate(email: Email, password: Password) { // Implementation } const email = validateEmail('user@example.com'); const password = hashPassword('secret123'); if (email) { authenticate(email, password); // Type-safe } Benefits: • Prevent mixing of similar but distinct types • Add semantic meaning to primitive types • Catch errors at compile time • Improve code documentation and clarity.

Question:
How do you implement a type-safe builder pattern?
Answer:
Type-safe builder pattern uses method chaining with conditional types to track which properties have been set. Basic Builder Pattern: interface User { id: string; name: string; email: string; age?: number; } type RequiredKeys = { [K in keyof T]-?: {} extends Pick ? never : K; }[keyof T]; type Builder = { [P in RequiredKeys]: P extends K ? Builder : (value: T[P]) => Builder; } & (RequiredKeys extends K ? { build(): T } : {}); class UserBuilder implements Builder { private user: Partial = {}; id(value: string) { this.user.id = value; return this; } name(value: string) { this.user.name = value; return this; } email(value: string) { this.user.email = value; return this; } build(): User { return this.user as User; } } // Usage const user = new UserBuilder() .id('123') .name('John') .email('john@example.com') .build(); Advanced Generic Builder: class GenericBuilder { private data: Partial = {}; set(key: K, value: T[K]) { this.data[key] = value; return this; } build(): T { return this.data as T; } } Benefits: • Ensures required properties are set before building • Provides excellent IDE autocomplete • Prevents runtime errors from missing properties • Creates fluent, readable APIs.

Question:
Explain the difference between structural and nominal typing systems.
Answer:
Structural typing compares type structure, while nominal typing compares type names. TypeScript uses structural typing. Structural Typing (TypeScript): interface Point2D { x: number; y: number; } interface Vector2D { x: number; y: number; } // These are compatible due to same structure let point: Point2D = { x: 1, y: 2 }; let vector: Vector2D = point; // OK in TypeScript Nominal Typing (Languages like Java/C#): // In Java, these would be different types class Point2D { int x, y; } class Vector2D { int x, y; } // Point2D point = new Vector2D(); // Error in Java TypeScript Duck Typing: function drawPoint(point: { x: number; y: number }) { console.log('Point at (' + point.x + ', ' + point.y + ')'); } // Any object with x and y properties works drawPoint({ x: 1, y: 2 }); // OK drawPoint({ x: 1, y: 2, z: 3 }); // OK (extra properties allowed) Implications: • Structural typing enables duck typing • More flexible but can allow unintended compatibility • Nominal typing is more strict but verbose • TypeScript approach enables JavaScript interoperability.

Question:
How do you create a type that validates function argument relationships?
Answer:
Use conditional types with infer to extract parameter types and validate relationships between arguments. Basic Parameter Validation: type ValidateArgs = T extends (...args: infer P) => any ? P : never; type FirstArg = T extends (first: infer F, ...rest: any[]) => any ? F : never; type SecondArg = T extends (first: any, second: infer S, ...rest: any[]) => any ? S : never; // Ensure second argument extends first type RelatedArgs = U extends T ? [T, U] : never; function processRelated(first: T, second: U): void { // Implementation } Advanced Validation: type ValidateFunctionArgs = F extends (...args: infer P) => any ? P extends [infer A, infer B] ? B extends A ? F : never : F : never; // Usage type ValidFunction = ValidateFunctionArgs<(base: string, extended: string) => void>; // OK type InvalidFunction = ValidateFunctionArgs<(base: string, extended: number) => void>; // never Benefits: • Compile-time argument validation • Ensures type relationships between parameters • Prevents invalid function signatures • Provides excellent type safety.

Question:
Explain advanced decorator patterns and metadata reflection.
Answer:
Decorators can store metadata using reflect-metadata library for dependency injection and validation. Basic Decorator: function Component(target: any) { Reflect.defineMetadata('component', true, target); } @Component class MyComponent { // Implementation } Property Decorators: function Inject(token: string) { return function (target: any, propertyKey: string) { Reflect.defineMetadata('inject', token, target, propertyKey); }; } class UserService { @Inject('DATABASE') private db: Database; } Method Decorators: function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log('Calling method:', propertyKey); return originalMethod.apply(this, args); }; } class Calculator { @Log add(a: number, b: number): number { return a + b; } } Metadata Reflection: function getInjectionTokens(target: any): string[] { const tokens: string[] = []; const propertyKeys = Object.getOwnPropertyNames(target.prototype); for (const key of propertyKeys) { const token = Reflect.getMetadata('inject', target.prototype, key); if (token) { tokens.push(token); } } return tokens; } Benefits: • Enable dependency injection • Provide runtime type information • Support aspect-oriented programming • Enable framework magic.

Question:
How do you implement compile-time validation of configuration objects?
Answer:
Use conditional types and mapped types to validate configuration structure at compile time. Basic Configuration Validation: type RequiredConfig = { apiUrl: string; timeout: number; retries: number; }; type ValidateConfig = { [K in keyof RequiredConfig]: K extends keyof T ? T[K] extends RequiredConfig[K] ? T[K] : never : never; }; function createClient(config: ValidateConfig) { // Implementation } // Usage createClient({ apiUrl: 'https://api.example.com', // OK timeout: 5000, // OK retries: 3 // OK }); // createClient({ apiUrl: 123 }); // Error: number not assignable to string Advanced Validation: type DeepValidate = { [K in keyof Schema]: K extends keyof T ? Schema[K] extends object ? T[K] extends object ? DeepValidate : never : T[K] extends Schema[K] ? T[K] : never : never; }; type DatabaseSchema = { host: string; port: number; credentials: { username: string; password: string; }; }; function connectDatabase(config: DeepValidate) { // Implementation } Benefits: • Catch configuration errors at compile time • Ensure required fields are provided • Validate nested object structures • Provide excellent IDE support.

Question:
Explain the concept of phantom types in TypeScript.
Answer:
Phantom types use type parameters that exist only at compile time for type safety. Basic phantom type: type Phantom = T & { __phantom: P }; type Meters = Phantom; type Feet = Phantom; function createMeters(value: number): Meters { return value as Meters; } function addMeters(a: Meters, b: Meters): Meters { return (a + b) as Meters; } Usage example: const m1 = createMeters(10); const m2 = createMeters(20); const result = addMeters(m1, m2); // OK // addMeters(m1, createFeet(30)); // Error Benefits: Compile-time safety, zero runtime cost, prevents value mixing.

Question:
How do you create a type-safe ORM query builder?
Answer:
Use mapped types for table schemas, conditional types for query validation, and method chaining with type narrowing. Basic Schema Definition: type User = { id: number; name: string; email: string; age: number; }; type Post = { id: number; title: string; content: string; userId: number; }; Query Builder Implementation: type WhereClause = { [K in keyof T]?: T[K] | { gt: T[K] } | { lt: T[K] } | { in: T[K][] }; }; type SelectFields = (keyof T)[]; class QueryBuilder { constructor(private tableName: string) {} select(...fields: K[]): QueryBuilder { // Implementation return new QueryBuilder(this.tableName); } where(conditions: WhereClause): QueryBuilder { // Implementation return this; } execute(): Promise[]> { // Implementation return Promise.resolve([]); } } // Usage const userQuery = new QueryBuilder('users') .select('id', 'name', 'email') .where({ age: { gt: 18 } }) .execute(); // Returns Promise[]> Advanced Join Support: type JoinResult = T & U; class AdvancedQueryBuilder { constructor(private tableName: string) {} join( table: string, leftKey: K, rightKey: L ): AdvancedQueryBuilder> { // Implementation return new AdvancedQueryBuilder>(this.tableName); } select(...fields: K[]): AdvancedQueryBuilder> { // Implementation return new AdvancedQueryBuilder>(this.tableName); } } // Usage with joins const joinQuery = new AdvancedQueryBuilder('users') .join('posts', 'id', 'userId') .select('name', 'title'); // Type: Pick Benefits: • Compile-time validation of table schemas • Type-safe field selection • Prevents invalid join conditions • Excellent IDE autocomplete for database operations.

Question:
Explain control flow analysis and type narrowing in TypeScript.
Answer:
TypeScript analyzes code flow to narrow types based on conditions, assignments, and type guards. Type Narrowing with typeof: function padLeft(padding: number | string, input: string): string { if (typeof padding === 'number') { // TypeScript knows padding is number here return new Array(padding + 1).join(' ') + input; } // TypeScript knows padding is string here return padding + input; } Truthiness Narrowing: function getUserName(user: { name?: string }) { if (user.name) { // TypeScript knows user.name is string (not undefined) return user.name.toUpperCase(); } return 'Anonymous'; } Discriminated Unions: type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; sideLength: number }; function getArea(shape: Shape) { switch (shape.kind) { case 'circle': // TypeScript knows shape has radius property return Math.PI * shape.radius ** 2; case 'square': // TypeScript knows shape has sideLength property return shape.sideLength ** 2; } } Custom Type Guards: function isString(value: unknown): value is string { return typeof value === 'string'; } function processValue(value: unknown) { if (isString(value)) { // TypeScript knows value is string console.log(value.toUpperCase()); } } Benefits: • Eliminates need for type assertions • Provides compile-time safety • Enables precise type checking • Improves code reliability.

Question:
How do you implement a type-safe event system with payload validation?
Answer:
Use discriminated unions for event types, mapped types for event-payload relationships, and conditional types for type-safe event handling. Basic Event System: type EventMap = { 'user:login': { userId: string; timestamp: Date }; 'user:logout': { userId: string }; 'post:created': { postId: string; authorId: string; title: string }; 'post:deleted': { postId: string }; }; type EventName = keyof EventMap; type EventPayload = EventMap[T]; class TypeSafeEventEmitter { private listeners: { [K in EventName]?: Array<(payload: EventPayload) => void>; } = {}; on( event: T, listener: (payload: EventPayload) => void ): void { if (!this.listeners[event]) { this.listeners[event] = []; } (this.listeners[event] as any[]).push(listener); } emit(event: T, payload: EventPayload): void { const eventListeners = this.listeners[event]; if (eventListeners) { eventListeners.forEach(listener => listener(payload)); } } } // Usage const emitter = new TypeSafeEventEmitter(); emitter.on('user:login', (payload) => { // payload is typed as { userId: string; timestamp: Date } console.log('User logged in:', payload.userId); }); emitter.emit('user:login', { userId: '123', timestamp: new Date() }); // Type-safe // emitter.emit('user:login', { userId: 123 }); // Error: number not assignable to string Advanced Event Validation: type ValidateEventPayload = T extends EventPayload ? T : never; function createTypedEvent( name: E, payload: ValidateEventPayload, E> ): { name: E; payload: EventPayload } { return { name, payload }; } // Usage const loginEvent = createTypedEvent('user:login', { userId: '123', timestamp: new Date() }); Benefits: • Compile-time validation of event payloads • Type-safe event emission and handling • Prevents invalid event data • Excellent IDE autocomplete for events.

Question:
Explain the module resolution strategies in TypeScript.
Answer:
TypeScript supports Node.js and Classic resolution strategies, configurable via moduleResolution option. Node.js Resolution Strategy: // For import { something } from 'moduleA' // TypeScript looks in this order: // 1. node_modules/moduleA/package.json (main field) // 2. node_modules/moduleA/index.ts // 3. node_modules/moduleA/index.tsx // 4. node_modules/moduleA/index.d.ts // 5. node_modules/moduleA/index.js // For relative imports like './utils' // 1. ./utils.ts // 2. ./utils.tsx // 3. ./utils.d.ts // 4. ./utils/package.json (main field) // 5. ./utils/index.ts // 6. ./utils/index.tsx // 7. ./utils/index.d.ts Classic Resolution Strategy: // For import { something } from 'moduleA' // 1. /root/src/moduleA.ts // 2. /root/src/moduleA.d.ts // 3. /root/moduleA.ts // 4. /root/moduleA.d.ts // 5. /moduleA.ts // 6. /moduleA.d.ts Configuration Options: { "compilerOptions": { "moduleResolution": "node", // or "classic" "baseUrl": "./src", "paths": { "@/*": ["./src/*"], "@components/*": ["./src/components/*"], "@utils/*": ["./src/utils/*"] }, "typeRoots": ["./node_modules/@types", "./src/types"], "types": ["node", "jest"] } } Path Mapping: // With baseUrl and paths configuration import { Button } from '@components/Button'; // Resolves to ./src/components/Button import { formatDate } from '@utils/date'; // Resolves to ./src/utils/date Type-Only Imports: // Explicit type-only imports (TypeScript 3.8+) import type { User } from './types/User'; import type * as UserTypes from './types/User'; // Mixed imports import { validateUser, type User } from './user'; Module Augmentation: // Extending existing modules declare module 'express' { interface Request { user?: User; } } Benefits: • Flexible module resolution strategies • Support for path mapping and aliases • Type-only imports for better tree shaking • Compatibility with different environments.

Question:
How do you create a type that validates API response schemas?
Answer:
Use conditional types to validate response structure, mapped types for field validation, and recursive types for nested object validation. Basic Response Validation: type APIResponse = { success: boolean; data: T; error?: string; }; type ValidateResponse = { [K in keyof Schema]: K extends keyof T ? T[K] extends Schema[K] ? T[K] : never : never; }; // Define expected schema type UserSchema = { id: number; name: string; email: string; }; function validateUserResponse( response: T ): ValidateResponse> { // Runtime validation logic here return response as ValidateResponse>; } Advanced Nested Validation: type DeepValidate = { [K in keyof Schema]: K extends keyof T ? Schema[K] extends object ? T[K] extends object ? DeepValidate : never : T[K] extends Schema[K] ? T[K] : never : never; }; type NestedUserSchema = { user: { id: number; profile: { name: string; settings: { theme: string; notifications: boolean; }; }; }; }; function validateNestedResponse( response: T ): DeepValidate { // Deep validation logic return response as DeepValidate; } Array Response Validation: type ArrayResponse = { items: T[]; total: number; page: number; }; type ValidateArrayResponse = { items: ValidateResponse[]; total: number; page: number; }; function validateArrayResponse( response: ArrayResponse ): ValidateArrayResponse { // Array validation logic return response as ValidateArrayResponse; } Runtime Integration: function createValidator(schema: Schema) { return function validate(data: T): ValidateResponse { // Runtime validation using schema // Could integrate with libraries like Joi, Yup, or Zod return data as ValidateResponse; }; } const userValidator = createValidator({ id: 'number', name: 'string', email: 'string' }); // Usage const apiResponse = await fetch('/api/user/123'); const jsonData = await apiResponse.json(); const validatedUser = userValidator(jsonData); Benefits: • Compile-time validation of API response structure • Type-safe data access after validation • Prevents runtime errors from invalid data • Integrates well with runtime validation libraries.

Question:
Explain advanced compiler options and their performance implications.
Answer:
Advanced compiler options affect type checking performance, build speed, and output quality. Performance-Critical Options: { "compilerOptions": { // Skip type checking of declaration files "skipLibCheck": true, "skipDefaultLibCheck": true, // Enable incremental compilation "incremental": true, "tsBuildInfoFile": ".tsbuildinfo", // Faster builds but less type safety "noEmitOnError": false, "isolatedModules": true, // Remove unused imports "importsNotUsedAsValues": "remove", "preserveValueImports": false } } Strict Mode Options: { "compilerOptions": { // Enable all strict checks "strict": true, // Individual strict options "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitReturns": true, "noImplicitThis": true, "noUncheckedIndexedAccess": true } } Output Control Options: { "compilerOptions": { // Control output format "target": "ES2020", "module": "ESNext", "moduleResolution": "node", // Source maps and debugging "sourceMap": true, "inlineSourceMap": false, "declarationMap": true, // Tree shaking support "sideEffects": false, "useDefineForClassFields": true } } Project References for Large Projects: { "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true }, "references": [ { "path": "./packages/core" }, { "path": "./packages/ui" } ] } Performance Monitoring: // Enable build timing tsc --build --verbose // Generate trace files tsc --generateTrace trace // Analyze with --listFiles tsc --listFiles Optimization Strategies: 1. Use skipLibCheck for faster builds 2. Enable incremental compilation 3. Use project references for monorepos 4. Exclude unnecessary files 5. Use isolatedModules with bundlers 6. Minimize use of complex conditional types 7. Prefer interfaces over type aliases 8. Use const assertions for better performance Build Tool Integration: // Webpack with ts-loader { test: /\.tsx?$/, use: { loader: "ts-loader", options: { transpileOnly: true, // Skip type checking projectReferences: true } } } // Use ForkTsCheckerWebpackPlugin for separate type checking Benefits: • Faster compilation times • Better memory usage • Improved developer experience • Scalable build architecture • Optimized output for production.

Question:
How do you implement a type-safe dependency injection container?
Answer:
Use generic constraints for service registration, conditional types for dependency resolution, and mapped types for service discovery. Basic DI Container: type ServiceMap = {}; type Constructor = new (...args: any[]) => T; class DIContainer { private services = new Map(); private singletons = new Map(); register(token: string, implementation: Constructor): void { this.services.set(token, implementation); } registerSingleton(token: string, implementation: Constructor): void { this.services.set(token, implementation); this.singletons.set(token, null); } resolve(token: string): T { if (this.singletons.has(token)) { let instance = this.singletons.get(token); if (!instance) { const Implementation = this.services.get(token); instance = new Implementation(); this.singletons.set(token, instance); } return instance; } const Implementation = this.services.get(token); return new Implementation(); } } Type-Safe Version: interface ServiceRegistry { UserService: UserService; EmailService: EmailService; DatabaseService: DatabaseService; } type ServiceToken = keyof ServiceRegistry; class TypeSafeDIContainer { private services = new Map(); register( token: K, implementation: Constructor ): void { this.services.set(token, implementation); } resolve(token: K): ServiceRegistry[K] { const Implementation = this.services.get(token); return new Implementation() as ServiceRegistry[K]; } } // Usage const container = new TypeSafeDIContainer(); container.register('UserService', UserServiceImpl); const userService = container.resolve('UserService'); // Type: UserService Decorator-Based DI: const INJECTION_TOKENS = new Map(); function Injectable(token: string) { return function (target: T) { return target; }; } function Inject(token: string) { return function (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) { const existingTokens = INJECTION_TOKENS.get(target) || []; existingTokens[parameterIndex] = token; INJECTION_TOKENS.set(target, existingTokens); }; } @Injectable('UserService') class UserService { constructor(@Inject('DatabaseService') private db: DatabaseService) {} } Benefits: • Compile-time type safety for service resolution • Prevents injection of wrong service types • Excellent IDE autocomplete • Supports decorator-based injection.

Question:
Explain the relationship between TypeScript and ECMAScript proposals.
Answer:
TypeScript often implements stage 3+ proposals early, providing feedback to TC39. Some features like decorators evolved differently than proposals. TypeScript's Role in ECMAScript Evolution: • TypeScript team participates in TC39 committee • Implements experimental features for community feedback • Provides real-world usage data for proposals • Sometimes influences proposal direction Stage-based Implementation: Stage 0-1: Strawman/Proposal - TypeScript rarely implements these - Too early and unstable Stage 2: Draft - TypeScript may implement with experimental flag - Helps gather community feedback Stage 3: Candidate - TypeScript often implements these - Likely to become standard - Examples: Optional chaining, nullish coalescing Stage 4: Finished - TypeScript implements quickly - Becomes part of standard JavaScript Notable Examples: Optional Chaining (Stage 4): // TypeScript implemented early const user = response?.data?.user?.name; Nullish Coalescing (Stage 4): // TypeScript supported before browsers const config = userConfig ?? defaultConfig; Decorators (Stage 2): // TypeScript has own implementation @Component class MyComponent {} Private Fields (Stage 4): // TypeScript supported with # syntax class User { #id: number; constructor(id: number) { this.#id = id; } } Top-Level Await (Stage 4): // TypeScript supported in modules const data = await fetch('/api/data'); Divergent Evolution: Decorators: - TypeScript implemented experimental version - TC39 proposal evolved differently - TypeScript now supports both versions Modules: - TypeScript had own module system - ES6 modules became standard - TypeScript adapted to support both Configuration for Proposals: { "compilerOptions": { "target": "ES2022", "experimentalDecorators": true, "emitDecoratorMetadata": true, "useDefineForClassFields": true } } Benefits: • Early access to future JavaScript features • Community feedback shapes proposals • Smooth transition when features standardize • TypeScript helps validate proposal viability.

Question:
How do you optimize TypeScript compilation for large projects?
Answer:

TypeScript compilation optimization involves proper configuration, project structure, and build strategies. Project References: { "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true }, "references": [ { "path": "./packages/core" }, { "path": "./packages/ui" }, { "path": "./packages/api" } ] } Incremental Compilation: { "compilerOptions": { "incremental": true, "tsBuildInfoFile": ".tsbuildinfo", "composite": true } } Performance Optimizations: { "compilerOptions": { "skipLibCheck": true, "skipDefaultLibCheck": true, "noEmitOnError": false, "isolatedModules": true, "importsNotUsedAsValues": "remove" }, "exclude": [ "node_modules", "dist", "**/*.test.ts", "**/*.spec.ts" ] } Build Optimization Strategies: // Use type-only imports import type { User } from "./types"; import { validateUser } from "./validators"; // Prefer interfaces over type aliases for better performance interface Config { apiUrl: string; timeout: number; } // Use const assertions for better tree shaking const API_ENDPOINTS = { users: "/api/users", posts: "/api/posts" } as const; Webpack/Build Tool Integration: // webpack.config.js module.exports = { resolve: { extensions: [".ts", ".tsx", ".js"] }, module: { rules: [ { test: /\.tsx?$/, use: { loader: "ts-loader", options: { transpileOnly: true, projectReferences: true } } } ] }, plugins: [ new ForkTsCheckerWebpackPlugin() ] }; Benefits: • Faster compilation times • Better memory usage • Improved developer experience • Scalable build architecture • Parallel compilation support.

Question:
Explain generics in TypeScript with examples.
Answer:
Generics allow creating reusable components that work with multiple types while maintaining type safety. Example: function identity(arg: T): T { return arg; } - T is a type parameter that can be any type. Generic interfaces: interface Array { [index: number]: T; }, Generic classes: class GenericClass { value: T; }, Generic constraints: limits T to string types. Generics enable type-safe, reusable code without sacrificing performance.

Question:
What is the difference between type and interface in TypeScript?
Answer:
Key differences: 1) Declaration merging - interfaces can be merged, types cannot, 2) Extensibility - interfaces use extends, types use intersection (&), 3) Computed properties - types support computed properties, interfaces do not, 4) Union types - types can represent unions, interfaces cannot directly, 5) Primitive types - types can alias primitives, interfaces cannot, 6) Performance - interfaces are generally faster for object types. Use interfaces for object shapes that might be extended, types for unions and computed properties.
Study Tips
  • Read each question carefully
  • Try to answer before viewing the solution
  • Practice explaining concepts out loud
  • Review regularly to reinforce learning
Share & Practice

Found this helpful? Share with others!

Feedback