Ace Your Interviews 🎯
Browse our collection of interview questions across various technologies.
What is TypeScript and how does it differ from JavaScript?
TypeScript is a statically typed superset of JavaScript developed by Microsoft. It adds optional static typing, interfaces, generics, enums, and decorators to JavaScript. TypeScript compiles to plain JavaScript — no TypeScript syntax runs in browsers or Node.js directly. The key difference: TypeScript catches type errors at compile time (before running), while JavaScript discovers them at runtime (often in production). TypeScript adds zero runtime overhead — the compiled JavaScript is identical to what you'd write manually.
What is type inference in TypeScript?
Type inference is TypeScript's ability to automatically determine the type of a value from context without an explicit annotation. const count = 0 — TypeScript infers number. const name = 'Arjun' — infers string. const arr = [1, 2, 3] — infers number[]. const first = (arr: number[]) => arr[0] — infers return type number | undefined. Inference works for variables, function return types, and generic type parameters. TypeScript's inference is strong enough that most local variables don't need explicit annotations.
What is the difference between 'interface' and 'type' in TypeScript?
Both describe the shape of an object. Differences: interfaces support declaration merging (two interface User declarations are combined into one) and classes can implement them. Type aliases are more flexible: they can define union types (string | number), intersection types (A & B), mapped types, and conditional types. Interfaces cannot express these. Convention: use interface for object shapes and class contracts, use type for unions, intersections, and complex type compositions.
What does 'strict: true' do in tsconfig.json?
strict: true enables a group of strict type-checking options: strictNullChecks (null and undefined are not assignable to other types), noImplicitAny (error when TypeScript infers any), strictFunctionTypes (function types checked correctly), strictPropertyInitialization (class properties must be initialized), and others. The most impactful is strictNullChecks — it prevents 'cannot read property x of null' errors by forcing you to check for null/undefined before accessing properties. Always enable strict: true from day one.
What is the difference between 'unknown' and 'any'?
Both represent 'I don't know this type'. The difference: any disables all type checking — you can call any method, access any property, assign to anything. unknown requires you to narrow the type before using it — you must check typeof, instanceof, or use a type guard to convince TypeScript what the value is. any is unsafe (TypeScript trusts your claim). unknown is safe (TypeScript requires you to prove the claim). Use unknown for external data (API responses, JSON.parse results), use any only when migrating legacy code.
What is a union type and when would you use it?
A union type (A | B | C) says a value can be one of several types. type Status = 'pending' | 'active' | 'banned' means status can only be those three strings. function formatInput(input: string | number) accepts either type. type SearchResult = User | Product | null means the result could be a User, Product, or null. Union types with a discriminant field (same property with different literal values, like { kind: 'circle' } | { kind: 'square' }) are called discriminated unions and are TypeScript's most powerful pattern for state modeling.
What are generics in TypeScript?
Generics are type parameters that make functions, interfaces, and classes work with any type while maintaining type safety. function first<T>(arr: T[]): T | undefined returns the same type as the array elements — if you pass User[], you get User | undefined. Without generics, you'd either use any (loses types) or write a separate function for each type (loses reusability). Generics parameterize types just as function parameters parameterize values: same code, different types.
What are TypeScript decorators and where are they used?
Decorators are a TypeScript (and ES proposal) feature for adding metadata and modifying class behavior at declaration time using @decorator syntax. TypeScript requires experimentalDecorators: true in tsconfig.json (or TypeScript 5+ for Stage 3 decorators). They're used heavily in: Angular (@Component, @Injectable, @NgModule), NestJS (@Controller, @Get, @Injectable, @Body), and TypeORM/Prisma (@Entity, @Column). Decorators wrap or modify the class/method/property they decorate, enabling frameworks to add routing, dependency injection, and database mapping declaratively.
What is type narrowing in TypeScript?
Type narrowing is TypeScript's ability to refine a type to a more specific type within a code block based on a conditional check. typeof input === 'string' narrows input from string | number to string in the true branch. x instanceof Error narrows x from unknown to Error. if (value !== null) narrows value from T | null to T. Discriminated union narrowing: if (shape.kind === 'circle') narrows shape to { kind: 'circle'; radius: number }. TypeScript tracks these narrowings and knows the more specific type within each branch.
What is the non-null assertion operator (!) and when should you use it?
The non-null assertion operator (!) after a value tells TypeScript 'I guarantee this is not null or undefined — trust me'. document.getElementById('app')! tells TypeScript the element definitely exists. It's useful when TypeScript can't infer that a value is non-null but you know it is. Overuse is dangerous: if you're wrong, you get a runtime null dereference. Use it only when you're certain the value exists and TypeScript can't verify it. Prefer explicit checks (if (x !== null)) or optional chaining (x?.property) when possible.
Explain the difference between Partial<T>, Required<T>, Pick<T, K>, and Omit<T, K>.
Partial<T> makes all properties optional — useful for update DTOs where you only send changed fields. Required<T> makes all properties required — reverse of Partial. Pick<T, K> selects a subset of properties — Pick<User, 'id' | 'name'> creates a type with only id and name. Omit<T, K> removes properties — Omit<Product, 'id' | 'createdAt'> creates a type without id and createdAt, useful for create DTOs. All four are built-in mapped types that transform existing types without duplication.
What is a discriminated union and what problem does it solve?
A discriminated union is a union type where each variant has a 'discriminant' property with a unique literal value. type Shape = { kind: 'circle'; radius: number } | { kind: 'rectangle'; width: number; height: number }. TypeScript uses the discriminant (kind) to narrow the type in switch/case or if statements. It solves the 'impossible states' problem: without discriminated unions, you might have { isLoading: boolean; data: User | null; error: string | null } where (isLoading: false, data: null, error: null) is representable but should be impossible. A discriminated union makes impossible states unrepresentable.
What is the 'infer' keyword in TypeScript conditional types?
infer introduces a type variable inside a conditional type's extends clause that is 'inferred' from the type being checked. type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never — if T is a function type, R captures its return type. type ElementType<T> = T extends (infer E)[] ? E : never — if T is an array, E captures the element type. infer enables extracting parts of complex types: the return type of a function, the element type of an array, the resolved value of a Promise.
How does TypeScript's structural typing work compared to nominal typing?
Structural typing means compatibility is determined by shape (the properties and their types), not by the declared name or class hierarchy. interface A { x: number } and interface B { x: number } are structurally identical — a value of type A can be assigned to a variable of type B even though they have different names. This differs from nominal typing (Java/C#) where you'd need explicit compatibility declarations. Structural typing makes TypeScript practical for JavaScript's dynamic patterns: any object with the right shape satisfies an interface without explicit declaration.
What is the difference between 'keyof' and 'typeof' in TypeScript?
typeof extracts the TypeScript type of a JavaScript value — typeof user is User (if user is declared as User), typeof config returns the object type with all inferred literal types. keyof extracts a union of all known property names of a type — keyof User is 'id' | 'email' | 'name' | ... They combine powerfully: typeof config returns the literal-typed object shape, keyof typeof config returns the union of its keys as literal types. This enables type-safe property access: function getConfig<K extends keyof typeof config>(key: K): typeof config[K].
How do you write a type guard function in TypeScript?
A type guard function uses the 'is' keyword in its return type: function isUser(value: unknown): value is User. The function's return type is a type predicate — when it returns true, TypeScript narrows the argument to User in the calling scope. The implementation must do actual runtime checking to justify the narrowing. Common pattern: check typeof, check required properties, check their types. Type guards are the type-safe alternative to as assertions for external data.
What are mapped types and give a real-world example?
Mapped types transform the properties of an existing type using the syntax { [K in keyof T]: TransformedType }. Real-world examples: Partial<T> is { [K in keyof T]?: T[K] }, Readonly<T> is { readonly [K in keyof T]: T[K] }, a form state type { [K in keyof User]: { value: User[K]; error: string | null } }, a loading state type { [K in keyof T]: { value: T[K]; isLoading: boolean } }. TypeScript 4.1+ adds key remapping: { [K in keyof T as Capitalize<string & K>]: T[K] } renames all keys to their capitalized versions.
How do you extend third-party types in TypeScript without modifying node_modules?
Module augmentation — declare the module and add your types inside. For global types (Window, Array): create a .d.ts file in src/@types/ and use declare global { interface Window { myProp: string } }. For specific modules (Express Request): declare module 'express-serve-static-core' { interface Request { user?: JwtPayload } }. The key requirement: the augmentation file must export at least one thing (export {} works) to be a module, or it will be treated as a global script and may cause conflicts. Always use interface for augmentation — interfaces can be merged, type aliases cannot.
What is the difference between 'as const' and 'Readonly<T>'?
Readonly<T> is a type transformation that makes all properties of T readonly at the type level. as const is an assertion that infers all properties as readonly literal types and infers arrays as readonly tuples. Key difference: Readonly<{ status: string }> gives { readonly status: string }. { status: 'active' } as const gives { readonly status: 'active' } (literal, not widened to string). as const preserves exact literal types; Readonly<T> only adds readonly, the values are still widened. Use as const for configuration objects and constant arrays where you want literal types.
How does conditional typing with distributive behavior work?
Conditional types distribute over union types when the checked type is a bare type parameter. type IsArray<T> = T extends any[] ? true : false. Applied to string | number[], TypeScript distributes: (string extends any[] ? true : false) | (number[] extends any[] ? true : false) = false | true = boolean. To prevent distribution, wrap the type parameter in a tuple: type IsArray<T> = [T] extends [any[]] ? true : false — now [string | number[]] is checked as a single unit. Understanding distributive conditional types is essential for writing generic type utilities.
What are declaration files (.d.ts) and when do you write them?
Declaration files contain only type information — no JavaScript implementation. They tell TypeScript about the types of JavaScript values without TypeScript source. When to write them:
For an npm library written in JavaScript with no @types package — write a .d.ts in src/@types/library-name/ to type its exports.
For a library you're publishing — tsc with declaration: true auto-generates .d.ts from your TypeScript source.
For global augmentation (extending Window, Array, or third-party module types). For 90% of npm packages, @types/package-name provides the declarations — check before writing your own.
Explain variance in TypeScript's function types and why it matters.
Variance describes how type compatibility flows through type constructors. In TypeScript, function parameter types are checked bivariantly by default (both covariantly and contravariantly) for practical compatibility, but with strictFunctionTypes: true, method-position functions are bivariant while standalone function types are contravariant in parameter types. Practically: a function (animal: Animal) => void is assignable to a position expecting (dog: Dog) => void (contravariant — accepting a supertype is safe). A function () => Dog is assignable to () => Animal (covariant return types). Understanding this matters when writing generic callback types — wrong variance allows unsafe assignments that cause runtime errors.
How would you implement a type-safe event emitter in TypeScript?
Use a generic interface mapping event names to their payload types: interface EventMap { 'user:login': { userId: string; ip: string }; 'order:created': { orderId: string; total: number } }. Then generic on, emit, off methods constrained to keyof EventMap: class TypedEventEmitter<T extends Record<string, unknown>> { on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void; emit<K extends keyof T>(event: K, payload: T[K]): void }. Instantiated as new TypedEventEmitter<EventMap>() — all events and their payloads are fully typed. Emitting 'user:login' with { total: 100 } would be a type error.
What is the TypeScript Compiler API and what can you build with it?
The TypeScript Compiler API provides programmatic access to TypeScript's parser, type checker, and emitter. You can: traverse the AST (Abstract Syntax Tree) to analyze code structure, use the TypeChecker to resolve types of any expression, create custom transformers that modify the AST during compilation, implement custom lint rules that understand TypeScript semantics (not just syntax), write code generators that produce TypeScript from schemas (Prisma does this). Applications: custom code generators, architectural enforcement (no direct imports between feature modules), migration tools (find all usages of deprecated APIs).
How do you design a TypeScript API that provides good IntelliSense to consumers?
Several techniques:
Use overloads to provide different completions based on argument types — TypeScript shows the matching overload in IntelliSense.
Use template literal types for string parameters — type EventName = 'user:' + string provides completion for known prefixes.
Use generics that infer from arguments — consumers get correctly typed return values without specifying T explicitly.
Add JSDoc @param and @returns comments — they appear in IntelliSense hover tooltips.
Use discriminated unions with known variants — TypeScript suggests specific values for the discriminant.
Provide @example tags in JSDoc — VS Code shows examples in hover documentation.
Explain how Prisma's type generation works and how to extend Prisma types.
Prisma generates TypeScript types from your schema.prisma file during npx prisma generate. For each model, Prisma generates a model type (Product), create/update input types (Prisma.ProductCreateInput), where clauses (Prisma.ProductWhereInput), and order by types (Prisma.ProductOrderByWithRelationInput). These types are fully generated from the schema — changing the schema regenerates the types. To extend Prisma types: use Prisma's validator() for type-safe query arguments, use namespace merging to add computed properties, or create wrapper types (type ProductWithSeller = Product & { seller: User }). For model extensions with methods, use prisma.$extends().
How do you implement recursive types in TypeScript and what are their limitations?
TypeScript supports recursive type aliases since TypeScript 3.7. type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }. For recursive mapped types: type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T. Limitations: TypeScript limits instantiation depth to prevent infinite recursion — complex recursive types can hit the 'Type instantiation is excessively deep' error. Workaround: add a depth counter generic parameter to halt recursion, or restructure to avoid deeply recursive type checking. Infinite recursive types (without a base case) are a compile error.
What is the 'satisfies' operator and how does it differ from type assertion?
The satisfies operator (TypeScript 4.9+) validates that a value satisfies a type without widening the value's type. const config = { theme: 'dark', fontSize: 14 } satisfies AppConfig — TypeScript validates config has all required AppConfig properties, but config.theme is still string literal 'dark' (not widened to string as it would be with const config: AppConfig = ...). With : AppConfig annotation, config.theme is widened to the type's string. With satisfies, the narrower inferred type is preserved. Use satisfies when you want validation without losing literal type information.
How do you write type-safe dependency injection without a framework?
Two patterns:
Constructor injection with interfaces — define IUserService and IProductService interfaces, inject via constructor: class OrderService { constructor(private users: IUserService, private products: IProductService) {} }. In tests: new OrderService(mockUserService, mockProductService).
Generic service locator — type Container<T extends Record<string, unknown>> = { get<K extends keyof T>(key: K): T[K] }. Define the service registry type: type Services = { userService: UserService; productService: ProductService }. const container: Container<Services> — container.get('userService') returns UserService. TypeScript enforces both the key exists and the return type is correct.
How do branded/nominal types work and when would you use them?
TypeScript's structural typing means string and string are always compatible, so function transfer(from: string, to: string) can't distinguish between user IDs and product IDs at the type level. Branded types create nominal-like types: type UserId = string & { readonly _brand: 'UserId' }. Now function transfer(from: UserId, to: UserId) rejects plain strings and ProductIds at compile time, preventing the wrong-argument-order bug. Implementation: a brand factory function function userId(s: string): UserId { return s as UserId } validates input. Cost: zero runtime overhead (the _brand property doesn't exist at runtime — it's a type fiction that TypeScript uses for checking).
Describe a strategy for migrating a 50,000-line JavaScript codebase to TypeScript.
Phase 1 (1 week): Add TypeScript infrastructure. tsconfig.json with allowJs: true, checkJs: false, strict: false. Rename one utility file to .ts, fix its errors. Add @typescript-eslint rules to track any usage. Phase 2 (4–8 weeks): Migrate leaves to roots. Start with shared types (create types/ directory), then utilities, then data access layer, then services, then controllers. Enable checkJs: true on migrated files. Track any count in CI — it must decrease each sprint. Phase 3 (2 weeks): Enable strict options. Enable strictNullChecks first (most impactful), fix errors. Then noImplicitAny, then the remaining strict options. Phase 4 (ongoing): Enable noUncheckedIndexedAccess, exactOptionalPropertyTypes. Every PR review checks for new any usage. Measure bugs found per migration sprint — the team motivation.