TypeScript Interview Questions & Answers (2026)
Complete TypeScript interview preparation guide.
Junior Question: What is TypeScript and why was it created?
Level: Junior | Category: Fundamentals | Time: 7 mins
Benefits
TypeScript is an open-source programming language developed by Microsoft.
It is a superset of JavaScript that adds static typing and compile-time
error checking.
- Type Safety
- Better IntelliSense
- Improved Refactoring
- Easier Maintenance
- Better Developer Experience
Interviewer Note: 💡 Does TypeScript run directly in browsers?
No. TypeScript must be transpiled into JavaScript.
Junior Question: What is the difference between any, unknown and never?
Level: Junior | Category: Fundamentals | Time: 7 mins
Expected Answer
let value: any = 10;
let userInput: unknown = 'John';
function throwError(): never {
throw new Error('Something went wrong');
}
| Type |
Description |
| any |
Disables type checking completely |
| unknown |
Safer alternative to any |
| never |
Function never returns |
Junior Question: Interface vs Type Alias
Level: Junior | Category: Fundamentals | Time: 7 mins
Differences
interface User {
id: number;
name: string;
}
type Employee = {
id: number;
name: string;
};
- Interfaces support declaration merging
- Types can represent unions
- Types can represent primitives
- Interfaces are preferred for object contracts
Mid-Level Question: What are Generics?
Level: Mid-Level | Category: Generics & Utilities | Time: 10 mins
Advantages
Generics allow writing reusable and type-safe code.
function identity<T>(value: T): T {
return value;
}
identity<string>('Hello');
identity<number>(100);
- Reusable Code
- Compile-time Type Safety
- Better IDE Support
Mid-Level Practical: Predict the Output
Level: Mid-Level | Category: Generics & Utilities | Time: 10 mins
Code Example
type User = {
name: string;
};
const user = {} as User;
console.log(user.name);
Expected Answer
Answer
The code compiles successfully but prints undefined.
Interviewer Note: 💡 Strong candidates explain that type assertions do not create runtime values.
Mid-Level Question: What are Utility Types?
Level: Mid-Level | Category: Generics & Utilities | Time: 10 mins
Common Utility Types
interface User {
id: number;
name: string;
}
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
type UserName = Pick<User, 'name'>;
- Partial
- Readonly
- Pick
- Omit
- Record
- Required
Senior Question: Explain Type Guards
Level: Senior | Category: Advanced Types | Time: 12 mins
Common Guards
function print(value: string | number) {
if (typeof value === 'string') {
console.log(value.toUpperCase());
}
}
Type Guards help TypeScript narrow types safely during runtime checks.
- typeof
- instanceof
- in operator
- Custom Guards
Senior Question: What are Conditional Types?
Level: Senior | Category: Advanced Types | Time: 12 mins
Expected Answer
type IsString<T> =
T extends string
? true
: false;
type A = IsString<string>;
type B = IsString<number>;
Conditional Types allow creating dynamic types based on conditions.
Senior Question: What are Mapped Types?
Level: Senior | Category: Advanced Types | Time: 12 mins
Real-world Usage
type ReadonlyType<T> = {
readonly [K in keyof T]: T[K];
};
Mapped Types transform existing types into new types.
- DTO Generation
- Readonly Objects
- API Response Models
- Form Models
Practical Coding Round: Build a Generic API Response Type
Level: Coding Challenge | Category: Generics & Utilities | Time: 15 mins
Code Example
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
const response: ApiResponse<string> = {
success: true,
data: 'User Created'
};
Expected Answer
Interview Follow-up
- How would you handle API errors?
- How would you support pagination?
- How would you model nested responses?
Architect Level Question: Why is TypeScript important for large-scale Angular applications?
Level: Architect | Category: Architecture & Design | Time: 15 mins
Benefits
- Compile-time validation
- Improved maintainability
- Safe refactoring
- Reduced production bugs
- Better team collaboration
Interviewer Note: 💡 "TypeScript is JavaScript with types."
Explains scalability, maintainability, IDE support,
architecture benefits and developer productivity.
Architect Level Practical: Design a Type-Safe Event System
Level: Architect | Category: Architecture & Design | Time: 15 mins
Code Example
interface Events {
login: {
userId: number;
};
logout: {
userId: number;
};
}
function emit<K extends keyof Events>(
event: K,
payload: Events[K]
) {
console.log(event, payload);
}
Expected Answer
What Interviewers Evaluate
- Generics Knowledge
- Type Inference Understanding
- Scalable Design Thinking
- Architecture Skills
Question #13: Explain Union vs Intersection types in TypeScript. Give examples of when to use each.
Level: Junior | Category: Fundamentals | Time: 5 mins
Expected Answer
These types allow composing complex structures from basic types:
- Union Types (
|): Represents a value that can be one of several types (either A OR B). Used when variables can receive different input formats.
- Intersection Types (
&): Combines multiple types into one, requiring the value to satisfy all combined interfaces (A AND B). Used to extend objects or mix in behaviors.
Code Example
// Union Type
type Status = 'success' | 'error' | 'loading';
let current: Status = 'success';
// Intersection Type
interface Loggable { log: () => void; }
interface Identifiable { id: string; }
type Entity = Loggable & Identifiable;
const obj: Entity = {
id: 'E101',
log: () => console.log('Log entry')
};
Interviewer Note: 💡 Junior candidates should explain that intersections merge object properties, whereas unions allow choosing alternative primitive values.
Question #14: What is Type Narrowing? Explain how typeof, instanceof, and the in operator are used for narrowing.
Level: Junior | Category: Fundamentals | Time: 5 mins
Expected Answer
Type Narrowing is the process of resolving a broad type (like a union type) into a more specific type within conditional execution blocks.
- typeof: Used to check primitive values (e.g.
typeof value === 'string').
- instanceof: Used to check class constructor allocations (e.g.
value instanceof Date).
- in operator: Checks if a specific property exists inside an object (e.g.
'role' in user).
Code Example
function processValue(val: string | Date | { role: string }) {
if (typeof val === 'string') {
console.log(val.toUpperCase()); // Narrowed to string
} else if (val instanceof Date) {
console.log(val.getFullYear()); // Narrowed to Date
} else if ('role' in val) {
console.log(val.role); // Narrowed to object
}
}
Interviewer Note: 💡 Check if the candidate knows that typeof null is 'object', which requires extra checks when narrowing nullish values.
Question #15: What is the difference between interface extensions (extends) and type intersections (&)?
Level: Junior | Category: Fundamentals | Time: 5 mins
Expected Answer
While similar, they differ in how they handle property conflict checks:
- Interface Extension (
extends): Enforces strict verification. If you extend an interface and redefine a property with an incompatible type, the compiler throws a compile error immediately.
- Type Intersection (
&): Merges properties. If property types conflict, it creates an never type for that property rather than throwing a compile error immediately.
Code Example
// Interface conflict throws compile error
interface A { id: string; }
// interface B extends A { id: number; } // Compile Error!
// Type intersection merges types (results in never)
type C = { id: string };
type D = { id: number };
type E = C & D; // E.id is 'string & number' which resolves to never!
Interviewer Note: 💡 Mention that interfaces are generally faster for the compiler to process than type intersections because their shapes are cached.
Question #16: What are Literal Types and Template Literal Types?
Level: Junior | Category: Fundamentals | Time: 5 mins
Expected Answer
Literal types restrict variables to specific exact values rather than general types:
- Literal Types: Limit values to specific strings, numbers, or booleans (e.g.
let role: 'admin').
- Template Literal Types: Built on string literal types, allowing you to combine literal segments using template string syntax (e.g.
`on-${Event}`).
Code Example
type Direction = 'left' | 'right';
type Margin = `margin-${Direction}`; // 'margin-left' | 'margin-right'
Interviewer Note: 💡 Check if the candidate knows how template literals improve the development of CSS utility libraries or state events.
Question #17: What is Type Assertions (as Type) and why is it preferred over type casting? What is double assertion (as unknown as Type)?
Level: Junior | Category: Fundamentals | Time: 5 mins
Expected Answer
Type Assertions tell the compiler to treat a value as a specific type, bypassing default type inference checks:
- Assertion vs Casting: In C# or Java, type casting performs runtime conversions. In TypeScript, assertions are compile-time directives and do not modify the compiled JavaScript outputs.
- Double Assertion: Required when types do not overlap (e.g., converting a string to a number). Since you cannot assert incompatible types directly, you must assert to
unknown or any first.
Code Example
const val = '123';
// const num = val as number; // Compile Error: no overlap!
const num = val as unknown as number; // Double assertion passes compiler
Interviewer Note: 💡 Warn the candidate: double assertions disable type safety checks and can hide runtime bugs.
Question #18: Explain the difference between readonly properties and the Readonly<T> utility type.
Level: Junior | Category: Fundamentals | Time: 5 mins
Expected Answer
Both enforce read-only properties, but differ in scope:
- readonly property: Applied to individual properties inside interface definitions or classes.
- Readonly<T>: A utility type that wraps an entire interface type, making all of its properties read-only automatically.
Code Example
interface User {
readonly id: string;
name: string;
}
type FullyReadonlyUser = Readonly<User>; // Both id and name are now read-only!
Interviewer Note: 💡 Emphasize that read-only checks are compile-time boundaries; objects can still be mutated in compiled JavaScript.
Question #19: Explain Enums. What is the difference between numeric enums, string enums, and const enum?
Level: Junior | Category: Fundamentals | Time: 5 mins
Expected Answer
Enums define collections of named constants:
- Numeric Enums: Store values as numbers (starting at 0). Support reverse mappings (looking up key names by value).
- String Enums: Store values as strings, providing clearer error output. Do not support reverse mappings.
- const enum: Inline constant values during compilation, generating no runtime objects. This reduces bundle size.
Interviewer Note: 💡 Ask the candidate why 'const enum' is preferred for library performance. (Answer: it removes IIFE runtime code generation).
Question #20: What are Generic Constraints? How do you restrict a generic type parameter using the extends keyword?
Level: Mid-Level | Category: Generics & Utilities | Time: 7 mins
Expected Answer
Generic Constraints restrict the types that can be passed to a generic parameter using the extends keyword.
This allows you to access specific properties (e.g. length) safely inside generic functions.
Code Example
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Safe to access length property
return arg;
}
logLength('hello'); // Works
// logLength(123); // Compile Error: number lacks a length property!
Interviewer Note: 💡 Verify they understand how constraints preserve type safety inside generic functions.
Question #21: What is the difference between Structural Typing and Nominal Typing? Why is TypeScript structurally typed?
Level: Mid-Level | Category: Advanced Types | Time: 7 mins
Expected Answer
These models determine type compatibility differently:
- Nominal Typing: Compatibility is based on explicit type declarations and names (like in C++ or Java).
- Structural Typing: Compatibility is based on the structure (shape) of the object. If two objects share the same properties and types, they are compatible, regardless of their declared names.
- TypeScript's Choice: TypeScript uses structural typing because it matches JavaScript's dynamic patterns, where objects are defined ad-hoc.
Code Example
class User { name!: string; }
class Product { name!: string; }
const user: User = new Product(); // Valid because shapes are identical!
Interviewer Note: 💡 Check if the candidate knows how structural typing impacts class comparisons in tests.
Question #22: Explain how the keyof and typeof operators work in TypeScript type declarations.
Level: Mid-Level | Category: Advanced Types | Time: 7 mins
Expected Answer
These operators are used to extract types from values or keys:
- keyof: Takes an object type and returns a union type of its keys.
- typeof: Used in type queries to extract the TypeScript type of an active variable or object instance.
Code Example
const config = { api: '/api', timeout: 5000 };
type ConfigType = typeof config; // { api: string, timeout: number }
type ConfigKeys = keyof ConfigType; // 'api' | 'timeout'
Interviewer Note: 💡 Distinguish between the TypeScript 'typeof' operator (extracts compile-time types) and the JavaScript 'typeof' operator (runtime type checks).
Question #23: What are Declaration Merging and Module Augmentation? How do they work?
Level: Mid-Level | Category: Advanced Types | Time: 7 mins
Expected Answer
These features allow extending existing type definitions:
- Declaration Merging: The compiler automatically merges multiple declarations sharing the same name (e.g., merging two interfaces with the same name).
- Module Augmentation: Allows adding properties to external modules by declaring them within a matching module definition wrapper.
Code Example
// Module Augmentation
import { AxiosRequestConfig } from 'axios';
declare module 'axios' {
interface AxiosRequestConfig {
retryCount?: number; // Augment retry properties safely
}
}
Interviewer Note: 💡 Ask about use cases: e.g. adding custom properties to Express request objects or third-party libraries.
Question #24: Explain the purpose of declaration files (.d.ts) and Ambient Declarations (declare module, declare global).
Level: Mid-Level | Category: Modules & Namespaces | Time: 7 mins
Expected Answer
These files and declarations provide type descriptions for code that does not have native TypeScript types:
- Declaration Files (
.d.ts): Files containing only type declarations. They generate no compiled JavaScript output. Used to provide types for compiled JavaScript libraries.
- Ambient Declarations: Tell the compiler that a variable or module exists externally (e.g. globally in the browser), preventing compile errors.
Code Example
// global.d.ts
declare global {
const APP_VERSION: string; // Declare global variable
}
Interviewer Note: 💡 Check if the candidate knows how declaration files help integrate legacy JavaScript libraries.
Question #25: What are namespaces in TypeScript? Why are they mostly replaced by ES Modules, and when are they still useful?
Level: Mid-Level | Category: Modules & Namespaces | Time: 6 mins
Expected Answer
Namespaces are an internal module system used to group related code under a single global object name.
- Replacement: ES Modules are now the standard module format, resolving dependencies at compile time and enabling tree-shaking.
- When Useful: Namespaces are still useful for grouping global type definitions inside declaration files (
.d.ts) without importing them explicitly.
Interviewer Note: 💡 Strong candidates explain that namespaces add global variables at runtime, which prevents efficient tree-shaking.
Question #26: Explain key tsconfig.json compiler options: strict, noImplicitAny, strictNullChecks, target, module, and moduleResolution.
Level: Mid-Level | Category: Build & tsconfig | Time: 8 mins
Expected Answer
These options configure the type checking and compilation behavior:
- strict: Enables all strict type-checking options.
- noImplicitAny: Throws errors on expressions where the type defaults to
any implicitly.
- strictNullChecks: Treats
null and undefined as distinct types, preventing runtime property reads on null references.
- target: Specifies the ECMAScript target version for output files (e.g.
ES2022).
- moduleResolution: Determines how TypeScript resolves module imports (e.g.
node16, bundler).
Interviewer Note: 💡 Ensure the candidate advocates for keeping 'strict' enabled to guarantee type safety in production.
Question #27: What is the difference between type type guards (using is) and normal boolean functions?
Level: Mid-Level | Category: Advanced Types | Time: 7 mins
Expected Answer
While both return booleans at runtime, they handle types differently during compilation:
- Boolean Function: Returns a boolean without notifying the compiler about type changes. The compiler does not narrow types inside conditional blocks.
- Type Guard (
is): Tells the compiler that if the function returns true, the variable is guaranteed to match the asserted type, enabling type narrowing.
Code Example
interface User { role: string; }
// Custom Type Guard
function isUser(obj: any): obj is User {
return obj && typeof obj.role === 'string';
}
Interviewer Note: 💡 Verify they understand that custom type guards must perform accurate runtime validations to prevent runtime errors.
Question #28: Explain the satisfies operator introduced in TS 4.9/5.0. How does it differ from type assertion (as)?
Level: Mid-Level | Category: TS 5.x Features | Time: 8 mins
Expected Answer
The satisfies operator validates that an object matches a specific type structure without changing its inferred type:
- satisfies: Validates the type structure but retains the most specific inferred type (e.g. preserving string literal types).
- as assertion: Forces the compiler to treat an object as the specified type, which can widen types and hide missing property errors.
Code Example
type Colors = 'red' | 'blue';
const config = {
red: '#ff0000',
blue: [0, 0, 255]
} satisfies Record<Colors, string | number[]>;
// Retains specific inferred types (preserves string methods for red)
config.red.toUpperCase();
Interviewer Note: 💡 Look for the candidate's understanding of how satisfies prevents type widening.
Question #29: What are Const Type Parameters (Const Generics) in TypeScript 5.0?
Level: Mid-Level | Category: TS 5.x Features | Time: 7 mins
Expected Answer
Const Type Parameters allow generic parameters to automatically infer read-only, literal types when passed arguments, removing the need for as const assertions.
Code Example
// Const generic parameter T
function getRoles<const T extends readonly string[]>(roles: T) {
return roles;
}
const list = getRoles(['admin', 'user']); // Inferred type is readonly ['admin', 'user']
Interviewer Note: 💡 A modern TS 5.0 feature. Check if they understand how it simplifies literal type inferences.
Question #30: Explain the infer keyword inside conditional types. How do you extract return types or promise resolution types using infer?
Level: Senior | Category: Advanced Types | Time: 11 mins
Expected Answer
The infer keyword is used inside conditional type checks to declare a type variable that can be inferred dynamically by the compiler.
Code Example
// Extract return type of a function
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Extract resolution type of a Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
Interviewer Note: 💡 Check if the candidate knows how to use 'infer' to write custom utility types.
Question #31: How do you create mapped types that modify property modifiers (e.g. removing readonly or making all optional properties required using -? and -readonly)?
Level: Senior | Category: Advanced Types | Time: 11 mins
Expected Answer
Mapped types can add or remove property modifiers (like readonly or ?) using prefix modifiers (+ or -):
- -readonly: Removes the read-only modifier, making properties writable.
- -?: Removes the optional modifier, making all properties required.
Code Example
type MutableRequired<T> = {
-readonly [K in keyof T]-?: T[K];
};
interface User {
readonly id: string;
name?: string;
}
type CustomUser = MutableRequired<User>; // id is mutable, name is required!
Interviewer Note: 💡 Ask about use cases: e.g. converting frozen configuration models to mutable objects in test suites.
Question #32: Explain modern ECMAScript decorators (supported natively in TS 5.0) vs legacy experimental decorators (experimentalDecorators).
Level: Senior | Category: Decorators & Metadata | Time: 11 mins
Expected Answer
TypeScript 5.0 introduced native support for ECMAScript decorators, replacing the legacy Microsoft decorators:
- Legacy Decorators: Require setting
experimentalDecorators: true. Use old parameter signatures and evaluate class members during design-time.
- Native Decorators: Match native TC39 standards. Use type-safe signatures and receive context objects detailing properties, accessors, and metadata.
Code Example
// Native Method Decorator
function log(target: any, context: ClassMethodDecoratorContext) {
console.log(`Decorated method: ${String(context.name)}`);
}
Interviewer Note: 💡 A modern TS 5.0 feature. Check if the candidate knows how native decorators differ from experimental ones.
Question #33: What is reflect-metadata, and how is it used in dependency injection frameworks (like NestJS or Angular)?
Level: Senior | Category: Decorators & Metadata | Time: 11 mins
Expected Answer
reflect-metadata is a polyfill library that adds metadata APIs to class definitions and class properties.
In DI frameworks (like Angular or NestJS), the compiler reads constructor parameter types using reflect-metadata, allowing the DI container to resolve dependencies automatically at runtime.
Interviewer Note: 💡 Ask: 'Does JavaScript support metadata reflection natively?' (Answer: No, it requires polyfills like reflect-metadata).
Question #34: How do you optimize TypeScript build performance in large monorepos? Detail incremental builds, project references, and skipLibCheck.
Level: Senior | Category: Build & tsconfig | Time: 11 mins
Expected Answer
Optimizing builds guarantees fast compilation times in large projects:
- Incremental Builds: Set
incremental: true in tsconfig to cache file dependency structures, compiling only modified files.
- Project References: Splits a monolithic workspace into independent projects (using `references`), allowing separate compilations.
- skipLibCheck: Skips type-checking declaration files (
.d.ts) of npm dependencies, reducing compile times.
Interviewer Note: 💡 Look for experience with monorepos (Nx/Turborepo) and build tuning.
Question #35: Explain Covariance and Contravariance in the TypeScript type compatibility system. How do they apply to functions?
Level: Senior | Category: Advanced Types | Time: 11 mins
Expected Answer
These terms define how type relationships behave under transformations:
- Covariance: Retains type relationships. A subtype can be assigned to a base type (e.g. object property types).
- Contravariance: Reverses type relationships. A base type can be assigned to a subtype. This applies to function parameters (under
strictFunctionTypes).
Function Compatibility Example
- Function returns are **covariant**: A function returning a Subtype is compatible with a signature returning a BaseType.
- Function arguments are **contravariant**: A function accepting a BaseType is compatible with a signature accepting a Subtype.
Interviewer Note: 💡 A deep type compatibility topic. Check if they understand how strictFunctionTypes alters argument checks.
Question #36: How do you implement recursive types in TypeScript? Show how to model deeply nested JSON data or deep readonly utility wrappers.
Level: Senior | Category: Advanced Types | Time: 10 mins
Expected Answer
Recursive types reference themselves in their definition, which allows modeling nested data structures:
Code Example
// Nested JSON structure type
type JSONValue =
| string
| number
| boolean
| null
| { [key: string]: JSONValue }
| JSONValue[];
// Deep Readonly utility
type DeepReadonly<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
};
Interviewer Note: 💡 Check if the candidate knows how recursive types handle deep mappings.
Question #37: How does TypeScript's Control Flow Analysis (CFA) handle type narrowing across closure boundaries, and how can it fail?
Level: Senior | Category: Advanced Types | Time: 11 mins
Expected Answer
Control Flow Analysis (CFA) narrows types dynamically based on execution paths.
- The Failure: When narrowing a variable and reference it inside a closure (e.g. an arrow function), the type narrowing is lost.
- Why it Fails: The compiler cannot guarantee when the closure will run. If the variable is mutated between declaration and execution, the type check is no longer valid.
Code Example
let val: string | null = 'hello';
if (val !== null) {
// Val is narrowed to string here
const callback = () => {
// val is string | null here because it could have mutated!
console.log(val.toUpperCase()); // Compile Error!
};
}
Interviewer Note: 💡 A tricky compiler edge case. Resolving this requires capturing the narrowed value in a 'const' variable.
Question #38: How do you design type-safe APIs in TypeScript? Explain how to validate runtime payloads while keeping compile-time type assertions (using Zod or io-ts).
Level: Senior | Category: API Design | Time: 11 mins
Expected Answer
TypeScript is erased at runtime, meaning compile-time type checks cannot prevent invalid API responses.
To design type-safe APIs, combine compile-time types with runtime validation using libraries like **Zod** or **io-ts**:
- Schema: Define validation schemas.
- Inference: Infer TypeScript types directly from the schemas, keeping types and validations in sync.
Code Example
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email()
});
// Infer type dynamically
type User = z.infer<typeof UserSchema>;
Interviewer Note: 💡 Strong candidates explain why using schema validation libraries is preferred over type assertions (as) for external API data.
Question #39: How do you design a type-safe generic router in TypeScript that maps route string templates to parameter types?
Level: Senior | Category: API Design | Time: 11 mins
Expected Answer
You can use template literal types and recursive inference to extract route parameter types directly from route path strings.
Code Example
type ExtractParams<T extends string> =
T extends `${string}/:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string }
: T extends `${string}/:${infer Param}`
? { [K in Param]: string }
: {};
// Usage
type Params = ExtractParams<'/users/:id/posts/:postId'>; // { id: string, postId: string }
Interviewer Note: 💡 Tests advanced template literal types and recursive generic parsing skills.
Question #40: What is declaration emitter performance bottlenecks, and how does the isolatedModules setting affect bundler compilation (Vite/ESBuild)?
Level: Senior | Category: Build & tsconfig | Time: 11 mins
Expected Answer
Tools like Vite and ESBuild compile TypeScript files individually without performing type-checking.
- isolatedModules: This setting warns you if you use features that require cross-file type analysis (e.g. ambient namespaces, const enums, or type-only exports).
- Resolution: Force clean imports/exports using
import type to allow bundlers to compile files independently and safely.
Interviewer Note: 💡 Good candidates discuss compilation workflows and build tool configurations.
Architect Question #14: Design a type-safe State Management engine in TypeScript that enforces action payloads and selectors at compile-time.
Level: Architect | Category: Architecture & Design | Time: 15 mins
Expected Answer
A type-safe state engine should map action types to their payloads statically, preventing dispatch of invalid payload shapes.
Code Example
interface ActionMap {
ADD_USER: { id: string; name: string };
DELETE_USER: { id: string };
}
class Store<S> {
dispatch<K extends keyof ActionMap>(action: K, payload: ActionMap[K]) {
console.log('Dispatching action:', action, payload);
}
}
Interviewer Note: 💡 Check if the candidate knows how to enforce type safety across actions and state selectors.
Architect Question #15: How do you enforce nominal-like typing in TypeScript (Opaque/Branded Types)? Explain their applications in security or domain validation.
Level: Architect | Category: Architecture & Design | Time: 15 mins
Expected Answer
TypeScript is structurally typed, which can allow mixing up different identifiers (e.g. passing a User ID to a Product ID argument).
To prevent this, use **Branded Types** to simulate nominal typing by attaching unique property flags to types.
Code Example
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'user'>;
type ProductId = Brand<string, 'product'>;
function deleteUser(id: UserId) {}
const userId = '101' as UserId;
const productId = '101' as ProductId;
// deleteUser(productId); // Compile Error: types are incompatible!
deleteUser(userId); // Works
Interviewer Note: 💡 Highly relevant for security-sensitive fields (e.g., distinguishing raw strings from sanitized HTML strings).
Architect Question #16: Design a type-safe micro-dependency injection container supporting scopes (Singleton, Transient) in TypeScript.
Level: Architect | Category: Architecture & Design | Time: 15 mins
Expected Answer
A DI container resolves dependencies recursively. It uses generic registrations to map token keys to target class constructors, ensuring that resolved instances match their registered classes.
Interviewer Note: 💡 Evaluate how the candidate handles circular dependency checks during resolution.
Architect Question #17: How do you architect a TypeScript migration strategy for a legacy JavaScript monorepo with 500,000 lines of code?
Level: Architect | Category: Migration & Scaling | Time: 15 mins
Expected Answer
Migrating a large codebase requires an incremental, step-by-step approach to avoid breaking builds:
- Phase 1: Tooling: Configure TypeScript to allow JavaScript files (
allowJs: true, checkJs: false) and set noImplicitAny: false.
- Phase 2: JSDoc Types: Add types to core libraries using JSDoc comments.
- Phase 3: Core Conversion: Convert critical utility and model files to TypeScript (
.ts).
- Phase 4: Strict Mode: Enable strict options (like
strictNullChecks) progressively across folders.
Interviewer Note: 💡 Check if the candidate recommends automated refactoring tools to speed up migrations.
Architect Question #18: Explain how the Dependency Inversion Principle (from SOLID) is architected using TypeScript interfaces and class constructors.
Level: Architect | Category: Architecture & Design | Time: 15 mins
Expected Answer
The Dependency Inversion Principle states that high-level modules should depend on abstractions (interfaces), not concrete implementations.
In TypeScript, interfaces do not exist in compiled JavaScript, so we must inject dependencies using Injection Tokens (using unique classes or symbols) to resolve implementations at runtime.
Interviewer Note: 💡 Strong candidates explain why class names can be used as injection tokens because they are preserved in compiled JavaScript.
Architect Question #19: Design a type-safe ORM schema mapper that dynamically maps database tables and column types to TypeScript model interfaces.
Level: Architect | Category: Architecture & Design | Time: 15 mins
Expected Answer
Map table schemas to types using generic types. We can define column types programmatically, allowing query methods to return type-safe row entities.
Interviewer Note: 💡 Look for their use of generic type constraints to map dynamic database rows.
Architect Question #20: Design a type-safe runtime validation middleware for Express/Node.js APIs that ensures request query, body, and params match TS interfaces.
Level: Architect | Category: API Design | Time: 15 mins
Expected Answer
Create a middleware factory that accepts a validation schema (like Zod). If validation fails, return a 400 response; if it passes, attach the validated object to the request with proper types.
Interviewer Note: 💡 Check if the candidate knows how to extend Express request type definitions using declaration merging.
Architect Question #21: Design a configuration loader that loads, validates, and freezes environment settings in a type-safe way.
Level: Architect | Category: Architecture & Design | Time: 15 mins
Expected Answer
Use a validation schema to parse environment variables, set default values, and freeze the output object (using Object.freeze) to prevent runtime modifications.
Interviewer Note: 💡 Look for their handling of missing variables and invalid data types during loading.
Angular Trap #13: Why does keyof any resolve to string | number | symbol? Explain key indexing limits.
Level: Tricky Question | Category: Advanced Types | Time: 10 mins
Expected Answer
In JavaScript, object keys are converted to strings, numbers, or symbols during lookup.
As a result, keyof any resolves to the union type string | number | symbol, which represents any valid object key type.
Interviewer Note: 💡 A tricky type question. Candidates should explain why object keys are restricted to these types.
Angular Trap #14: What is the danger of using the Non-Null Assertion Operator (!)? How does it bypass compiler checks, and what are safe alternatives?
Level: Tricky Question | Category: Advanced Types | Time: 10 mins
Expected Answer
The non-null assertion operator (!) tells the compiler that a variable is not null or undefined, bypassing strict null checks.
- The Hazard: It does not perform runtime checks. If the value is nullish at runtime, the code will fail with an error.
- Alternatives: Use optional chaining (
?.), nullish coalescing (??), or type narrowing checks.
Interviewer Note: 💡 Check if the candidate knows when to avoid using the non-null assertion operator.
Angular Trap #15: Why are TypeScript private variables accessible at runtime, and how do native ES #private fields resolve this?
Level: Tricky Question | Category: Fundamentals | Time: 10 mins
Expected Answer
TypeScript's private modifier is a compile-time check; compiled JavaScript classes do not enforce private properties, making them accessible at runtime.
- ES Private Fields: Native private fields (using
#) are enforced by the JavaScript engine at runtime. Accessing them from outside the class throws a syntax error.
Interviewer Note: 💡 Ask about differences in runtime visibility and browser compatibility.
Angular Trap #16: Why does TypeScript allow assigning an object with extra properties to an interface parameter, but throws an error when passed directly as an object literal?
Level: Tricky Question | Category: Advanced Types | Time: 10 mins
Expected Answer
This difference is caused by **Excess Property Checks**:
- Object Literal: Passed directly as a parameter, the compiler performs strict check to catch typos, throwing errors if extra properties are found.
- Variable Assignment: Assigned to a variable first, compatibility is checked structurally. If the required properties exist, extra properties are allowed and ignored.
Interviewer Note: 💡 Tricky property check scenario. Check if they understand how structural compatibility differs from object literal validation.
Angular Trap #17: Why is Record<string, unknown> preferred over object and {} for representing key-value configurations?
Level: Tricky Question | Category: Generics & Utilities | Time: 10 mins
Expected Answer
These types differ in key indexing capabilities:
{}: Represents any non-nullish value, which can prevent accessing properties dynamically.
object: Represents any non-primitive type, but does not allow key lookup accesses.
Record<string, unknown>: Explicitly defines an object with string keys, allowing property access while enforcing type safety.
Interviewer Note: 💡 A tricky type definition question. Look for a clear explanation of indexing limitations.
Angular Trap #18: Why does Omit<T, K> disable auto-completion for key names, and how do you write a custom Omit type that preserves key autocompletion?
Level: Tricky Question | Category: Generics & Utilities | Time: 10 mins
Expected Answer
The second parameter of Omit<T, K> is typed as any string, which disables key auto-completion in IDEs.
Solution: Restrict keys using keyof T in your custom type definition:
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
Interviewer Note: 💡 Check if the candidate knows how to use generic constraints to preserve IDE autocompletion.
Angular Trap #19: Why does using readonly inside an index signature still allow mutating properties of nested object arrays?
Level: Tricky Question | Category: Advanced Types | Time: 10 mins
Expected Answer
Like shallow copying, the readonly modifier is shallow. It only prevents re-assigning top-level properties. If a property is a reference type (like an array or object), its nested properties can still be mutated.
Interviewer Note: 💡 Explain that DeepReadonly is needed to make nested properties immutable.
Angular Trap #20: Why does the as const assertion freeze literal types, and how does it affect arrays and object properties?
Level: Tricky Question | Category: Advanced Types | Time: 10 mins
Expected Answer
The as const assertion sets literal types as read-only and prevents type widening:
- Arrays are converted to read-only tuples.
- Object properties are set as read-only.
- Strings, numbers, and booleans are restricted to their exact literal values.
Interviewer Note: 💡 Verify they understand how as const improves literal type definitions.
Production Incident #7: TypeScript application compiles successfully but crashes at runtime due to wrong type assertions in API payloads
Level: Production Incident | Category: Production Incidents | Time: 15 mins
Scenario
An application compiled without errors, but crashed in production with Cannot read property of undefined when processing API responses.
Root Cause Analysis
The developer used type assertions (as User) on an HTTP request promise. Since assertions are compile-time directives, they do not validate the API payload shape at runtime. If the API returns unexpected data shapes, the code will fail with runtime errors.
Resolution
- Validate API response shapes at runtime using schema validation libraries like Zod or io-ts before processing data.
Interviewer Note: 💡 Tests runtime type safety awareness. Candidates should explain why type assertions do not guarantee runtime safety.
Production Incident #8: Monorepo compile times spike from 30 seconds to 10 minutes due to recursive type definitions and complex mapped types
Level: Production Incident | Category: Production Incidents | Time: 15 mins
Scenario
After adding complex utility types to a shared library, build times spiked across all applications in the monorepo.
Root Cause Analysis
The shared library introduced deeply recursive mapped types. These types require excessive calculations by the compiler to resolve properties, causing compilation bottlenecks.
Resolution
- Simplify recursive types or use interfaces instead of complex type aliases where possible.
- Analyze compiler performance using
tsc --extendedDiagnostics to identify slow type checks.
Interviewer Note: 💡 Look for experience with compiler profiling and monorepo build tuning.
Production Incident #9: Decoupled client bundle crashes on browser loading due to importing ambient declarations from .d.ts files that were not built
Level: Production Incident | Category: Production Incidents | Time: 15 mins
Scenario
The application builds successfully, but crashes on page load with ReferenceError: MyNamespace is not defined.
Root Cause Analysis
The developer used ambient namespaces inside declaration files (.d.ts) but referenced them as runtime values instead of type-only definitions. Since declarations are erased during compilation, accessing them at runtime throws ReferenceErrors.
Resolution
- Only use declaration files for type declarations. Never export runtime values or variables from declaration files.
Interviewer Note: 💡 Tests compilation boundaries. Candidates should explain why declarations must not export runtime values.
Production Incident #10: Runtime error Cannot read property of undefined inside lazy-loaded features due to wrong tsconfig path mapping aliases
Level: Production Incident | Category: Production Incidents | Time: 15 mins
Scenario
The compiler resolves imports successfully, but the application throws import errors at runtime when loading lazy-routed modules.
Root Cause Analysis
The developer configured path mapping aliases (e.g. @core/*) in tsconfig.json, but did not configure the bundler (like Webpack or Vite) to resolve these aliases in the output bundle.
Resolution
- Ensure path mappings are configured in both
tsconfig.json and your bundler config (e.g. using `tsconfig-paths-webpack-plugin` or `vite-tsconfig-paths`).
Interviewer Note: 💡 Check if the candidate knows how path mappings are resolved by compilers and bundlers.
Production Incident #11: Decorators fail to run inside Node.js ESM runtime due to compilation target misalignments
Level: Production Incident | Category: Production Incidents | Time: 15 mins
Scenario
A Node.js backend using class decorators builds without errors, but fails to run inside the ESM runtime, throwing syntax errors.
Root Cause Analysis
ESM runtimes verify imports strictly. If compile targets (e.g. `target`) or decorator configurations are misaligned, Node.js cannot parse decorators at runtime.
Interviewer Note: 💡 Look for experience with Node.js and ESM compilation targets.
Production Incident #12: Bundled code size doubles because compiler options output namespaces as full IIFE closures instead of tree-shakeable modules
Level: Production Incident | Category: Production Incidents | Time: 15 mins
Scenario
After importing a shared module, the client bundle size doubled unexpectedly.
Root Cause Analysis
The shared module used namespaces instead of ES Modules. Namespaces compile to IIFE closures, which bundlers cannot analyze statically for tree-shaking, including unused code in the final bundle.
Resolution
- Refactor namespaces to ES Modules (using `export`/`import` statements) to enable tree-shaking and bundle optimization.
Interviewer Note: 💡 Tests bundle optimization strategies. Candidates should explain why namespaces prevent tree-shaking.
Coding Challenge #3: Implement a generic DeepRequired<T> type that makes all properties (and nested properties) required.
Level: Coding Challenge | Category: Generics & Utilities | Time: 20 mins
Expected Solution
Implement a recursive mapped type that removes the optional modifier (-?) from all properties and nested objects.
Code Example
type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object
? DeepRequired<T[K]>
: T[K];
};
Interviewer Note: 💡 Check if the candidate handles nested objects correctly using recursive mapping.
Coding Challenge #4: Build a type-safe event emitter class in TypeScript where events and payload mappings are statically enforced.
Level: Coding Challenge | Category: Generics & Utilities | Time: 20 mins
Expected Solution
Create an EventEmitter class that accepts a generic event contract map, enforcing event payload types statically.
Code Example
class TypedEmitter<Events extends Record<string, any>> {
private listeners = new Map();
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void) {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event).add(listener);
}
emit<K extends keyof Events>(event: K, payload: Events[K]) {
const list = this.listeners.get(event);
if (list) list.forEach(fn => fn(payload));
}
}
Interviewer Note: 💡 Verify they enforce payload types statically using generic key mapping.
Coding Challenge #5: Implement a Join<T, D> template literal type that joins an array of string literals with a separator.
Level: Coding Challenge | Category: Generics & Utilities | Time: 20 mins
Expected Solution
Implement a recursive template literal type that joins elements of a string array using a separator character.
Code Example
type Join<T extends readonly string[], D extends string> =
T extends readonly [] ? ''
: T extends readonly [infer F extends string]
? F
: T extends readonly [infer F extends string, ...infer R extends string[]]
? `${F}${D}${Join<R, D>}`
: string;
// Usage
type Joined = Join<['a', 'b', 'c'], '-'>; // 'a-b-c'
Interviewer Note: 💡 Tests advanced template literal types and recursive inference.
Coding Challenge #6: Create a DeepPartial<T> utility type that handles nested objects, arrays, and primitive maps.
Level: Coding Challenge | Category: Generics & Utilities | Time: 20 mins
Expected Solution
Implement a recursive mapped type that makes all properties (and nested properties) optional.
Code Example
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
Interviewer Note: 💡 Check if the candidate knows how recursive type mapping handles deep nested structures.
Coding Challenge #7: Implement a type-safe configuration merger function that deep-merges two config objects.
Level: Coding Challenge | Category: Generics & Utilities | Time: 20 mins
Expected Solution
Write a deep-merging function in TypeScript that merges two config objects, returning an intersected type of both parameters.
Code Example
function deepMerge<T extends object, U extends object>(target: T, source: U): T & U {
const result = { ...target } as any;
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null && typeof target[key as any] === 'object') {
result[key] = deepMerge(target[key as any] as any, source[key] as any);
} else {
result[key] = source[key];
}
}
}
return result;
}
Interviewer Note: 💡 Verify they return the intersection type T & U to preserve type safety.
Coding Challenge #8: Build a Promisify<T> utility type that maps object methods returning standard callbacks to modern Promise return patterns.
Level: Coding Challenge | Category: Generics & Utilities | Time: 20 mins
Expected Solution
Create a mapped type that updates function signatures: if a function takes a callback as its last argument, return a Promise instead.
Interviewer Note: 💡 Look for their use of conditional types and key mapping to transform function signatures.
Anti-AI Scenario #3: Why is using Record<keyof any, any> in a generic type constraint a fatal mistake when mapping over literal union keys?
Level: Anti-AI Validation | Category: Advanced Types | Time: 12 mins
Expected Answer
Using Record<keyof any, any> widens key mappings to general string index signatures, losing specific literal key types.
As a result, mapped types cannot resolve literal keys, disabling compile-time autocomplete checks.
Resolution: Restrict keys using string or specific literal unions (e.g. K extends keyof any) to preserve type definitions.
Interviewer Note: 💡 Tests generic key boundaries. AI models often suggest generic Record constraints, ignoring key widening side-effects.
Anti-AI Scenario #4: Why does a mapped type with key remapping ([K in keyof T as NewKey]) lose type constraints when operating on generic objects?
Level: Anti-AI Validation | Category: Advanced Types | Time: 12 mins
Expected Answer
Key remapping (as NewKey) evaluates keys dynamically.
When operating on generic types, the compiler cannot verify the key relationships at compile time, which widens keys to string and loses type constraints.
Interviewer Note: 💡 Tests compiler resolution limitations. Candidates should explain why dynamic key remapping widens generic keys.
Anti-AI Scenario #5: Why does using any inside a conditional type check (like T extends any) trigger distributive conditional behavior, and how do you disable it?
Level: Anti-AI Validation | Category: Advanced Types | Time: 12 mins
Expected Answer
By default, if the checked type is a naked generic parameter, conditional types **distribute** over union elements (evaluating each union item individually).
- How to Disable: Wrap the extends check parameters in square brackets (
[T] extends [any]) to disable distribution and evaluate the union as a single type.
Code Example
// Distributive (evaluates string | number individually)
type Distributive<T> = T extends any ? T[] : never;
// Non-distributive (evaluates the union as a single type)
type NonDistributive<T> = [T] extends [any] ? T[] : never;
Interviewer Note: 💡 Tests union distribution mechanisms. A classic TypeScript compiler edge case.
Anti-AI Scenario #6: Why does the compiler fail to compile deep nested template literal types, throwing 'Type instantiation is excessively deep and possibly infinite', and how do you optimize it?
Level: Anti-AI Validation | Category: Advanced Types | Time: 12 mins
Expected Answer
TypeScript enforces a recursion depth limit (typically 50-100 stack frames) to prevent compiler hangs.
Deep recursive template literal types quickly exceed this limit, throwing the instantiation error.
Optimization: Flatten recursion steps or use tail-call recursive templates (matching patterns where the accumulator is passed in the tail position), which allows the compiler to optimize stack usage.
Interviewer Note: 💡 Ask about tail-call optimization in TypeScript types. (Answer: it allows the compiler to handle deeper recursive types without blowing the stack).
Anti-AI Scenario #7: Why does injecting a generic class dependency dynamically inside constructor providers resolve to Object instead of the generic type during runtime evaluation?
Level: Anti-AI Validation | Category: Decorators & Metadata | Time: 12 mins
Expected Answer
JavaScript engines do not support generics natively.
During compilation, TypeScript erases all generic parameters. As a result, metadata reflection queries (via `reflect-metadata`) resolve class parameters to their base compiled type (Object), losing the generic context.
Interviewer Note: 💡 Check if the candidate knows to use injection tokens rather than class type structures for generic dependency resolutions.
Anti-AI Scenario #8: Why does compiling TypeScript with isolatedModules: true prevent using ambient namespace declarations across module files, and how does it break monorepo bundles?
Level: Anti-AI Validation | Category: Build & tsconfig | Time: 12 mins
Expected Answer
With isolatedModules: true, compilers (like ESBuild or Babel) process files individually without reading other files.
Because ambient namespaces rely on cross-file declaration lookups, the compiler cannot verify them, causing compile errors. This prevents compiling namespaces as modular entities in monorepo bundles.
Interviewer Note: 💡 Verify they understand compilation boundaries when using modern transpilers.
Anti-AI Scenario #9: Why does combining a mapped type with optional modifier removal (-?) break object type inference when mapping over dynamic class getters, and how do you resolve it?
Level: Anti-AI Validation | Category: Advanced Types | Time: 12 mins
Expected Answer
Mapped types operate on property descriptors.
Removing the optional modifier (-?) on dynamic class getters can widen properties to include undefined, which breaks type inference for class methods.
Resolution: Filter out getters using key remapping (as) or map properties explicitly to prevent modifying class method signatures.
Interviewer Note: 💡 Tests property descriptor mappings and getters type inferences.
Anti-AI Scenario #10: Why does defining a type using infer inside a method parameter signature resolve to an intersection type rather than a union type?
Level: Anti-AI Validation | Category: Advanced Types | Time: 12 mins
Expected Answer
This behavior is caused by **Contravariance**:
- Function returns: Covariant. Inferred type variables resolve to **union types** because outputs are additive.
- Function parameters: Contravariant. Inferred type variables resolve to **intersection types** because parameters must satisfy all possible usage contracts simultaneously.
Interviewer Note: 💡 A deep type compatibility topic. Check if they understand how contravariance affects type inferences in parameter signatures.
Anti-AI Scenario #11: Why does a conditional type that checks T extends never return never instead of the expected fallback when passed never as a parameter?
Level: Anti-AI Validation | Category: Advanced Types | Time: 12 mins
Expected Answer
In conditional types, never is treated as an empty union.
When a union is evaluated distributively, an empty union has nothing to distribute over, so the check returns never immediately without evaluating the conditional branches.
Resolution: Wrap the parameter in square brackets ([T] extends [never]) to disable distribution and evaluate the parameter correctly.
Interviewer Note: 💡 Tests union distribution mechanisms. A classic TypeScript compiler edge case.
Anti-AI Scenario #12: Why does assigning a function returning void to a callback property typed as returning undefined throw a compilation error?
Level: Anti-AI Validation | Category: Advanced Types | Time: 11 mins
Expected Answer
In TypeScript, void and undefined represent different values:
undefined: An active JavaScript value. A function typed as returning undefined **must** return undefined explicitly.
void: Represents a function where the return value is ignored. Assigning a void-returning function to an undefined-returning callback throws errors because the compiler cannot guarantee the return value is undefined.
Interviewer Note: 💡 Check if the candidate knows how void differs from undefined in return statements.
Anti-AI Scenario #13: Why does using satisfies operator preserve literal type bindings whereas type assertions (as) widen them, and how does this affect object key lookups?
Level: Anti-AI Validation | Category: TS 5.x Features | Time: 12 mins
Expected Answer
The satisfies operator validates the type structure without changing its inferred type, which preserves specific literal value types.
Type assertions (as) force the compiler to treat the object as the asserted type, which widens literal properties to base types (e.g. 'red' becomes string), disabling exact key lookups.
Interviewer Note: 💡 Check if they understand how satisfies prevents type widening during key lookups.
Anti-AI Scenario #14: Why does declaring an interface with the same name as a namespace allow declaration merging, but declaring a type alias with the same name throws a duplicate identifier error?
Level: Anti-AI Validation | Category: Modules & Namespaces | Time: 12 mins
Expected Answer
According to declaration merging specifications, namespaces can merge with interfaces and classes because they represent different entities (namespaces create values; interfaces represent types).
Type aliases are static declarations and cannot merge with other declarations, throwing duplicate identifier errors if names conflict.
Interviewer Note: 💡 Tests declaration merging rules. Look for a clear distinction between types and values.