TypeScript Interview Questions
TypeScript interview questions test your understanding of static typing, type inference, and advanced type system features. They are commonly asked for frontend, backend, and full-stack roles to ensure you can write scalable, maintainable code. Expect a mix of conceptual explanations and hands-on coding problems.
What TypeScript interviews cover
Types and Interfaces
Understand the difference between type aliases and interfaces, and when to use each. Covers object types, function signatures, and declaration merging.
Generics
Know how to create reusable, type-safe components and functions using generic parameters. Includes constraints and default types.
Type Inference and Narrowing
Demonstrate how TypeScript infers types and narrows them using conditionals, discriminated unions, and type guards.
Utility and Conditional Types
Leverage built-in utility types (e.g., Partial, Pick) and create custom conditional types to transform types dynamically.
Sample TypeScript interview questions
- Explain the difference between `type` and `interface` in TypeScript. When would you use one over the other?What a strong answer covers
- Interfaces can be merged across multiple declarations; types cannot.
- Types can represent primitives, unions, tuples, and computed properties; interfaces are limited to object shapes.
- Interfaces benefit from class 'implements' and declaration merging; types are more flexible for complex type aliases.
View a sample answer
Use 'interface' when you need declaration merging (e.g., extending third-party types) or when defining objects that will be implemented by classes. Use 'type' for unions, intersections, primitives, tuples, or when you need computed properties (e.g., mapped types). Types are also preferred for callback signatures and conditional types. Both are structurally identical, so choose based on expressiveness and intent. Avoid mixing without reason.
- Implement a generic `DeepReadonly<T>` type that makes all properties and nested properties readonly.What a strong answer covers
- Recursively apply readonly to all properties using mapped types.
- Handle the base case when a property value is not an object.
- Use conditional types to differentiate between object and primitive types.
View a sample answer
DeepReadonly recursively makes all properties (including nested objects) readonly. It uses a mapped type over the keys of T, and for each property value, checks if it is an object (but not a function or array) to recurse. Functions and arrays are left unchanged to avoid runtime issues. The implementation requires careful handling of primitives and special types like Date. A common pitfall is infinite recursion on self-referential types, which can be mitigated by limiting depth or using a marker.
Reference solutiontypescript type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? T[P] extends Function ? T[P] : T[P] extends readonly any[] ? DeepReadonly<T[P]> : DeepReadonly<T[P]> : T[P]; }; // Time complexity: O(n) where n is number of properties recursively // Space complexity: O(n) for the resulting type - Write a generic function `filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[]` that filters based on a type predicate.What a strong answer covers
- Use a type predicate (item is T) to narrow the resulting array type.
- The function signature returns T[], but the predicate determines which items pass.
- Generic constraint ensures the predicate returns a type predicate on T.
View a sample answer
The function filterArray takes an array of type T and a predicate that returns a type predicate (item is T). This allows TypeScript to narrow the output type to T[] even if the predicate filters based on a subtype. The implementation simply calls Array.prototype.filter with the predicate. However, note that using a type predicate requires the predicate to explicitly return a boolean assertion; it must be a user-defined type guard. A common mistake is to use a regular boolean function, which would not narrow the type.
Reference solutiontypescript function filterArray<T>( arr: T[], predicate: (item: T) => item is T ): T[] { return arr.filter(predicate); } // Example usage: const mixed: (string | number)[] = [1, 'a', 2]; const strings = filterArray(mixed, (item): item is string => typeof item === 'string'); // strings: string[] // Time complexity: O(n) // Space complexity: O(n) for new array - What is the `unknown` type and how does it differ from `any`? Provide an example of when to use each.What a strong answer covers
- unknown is type-safe: any operation on it requires a type check; any bypasses all checks.
- unknown can be assigned to anything, but cannot be assigned to other types without assertion.
- Use unknown for values whose type you don't know at compile time, e.g., API responses; use any only when migrating legacy code or when type safety is impossible.
View a sample answer
The unknown type is the type-safe counterpart of any. A variable of type unknown can hold any value, but you cannot perform operations on it without first narrowing its type (e.g., with typeof, instanceof, or type guards). In contrast, any allows any operation without checks, effectively opting out of type safety. Use unknown when you need to accept arbitrary values but want to enforce type checking before use, such as parsing JSON data. Use any sparingly, only in scenarios where the type system cannot express the constraints (e.g., when interoperating with untyped JavaScript libraries).
- How would you type an async function that returns a Promise of a user object? Show how to handle the resolved and rejected states.What a strong answer covers
- Define a User type with required fields.
- Type the async function as () => Promise<User>.
- Handle success with .then or await, and errors with .catch or try-catch.
View a sample answer
To type an async function that returns a Promise of a user object, first define a User interface. Then declare the function as async and return a Promise<User>. Inside the function, use try-catch to handle both resolved and rejected states: await the asynchronous operation, and return the user on success. On error, you can either throw or return a special error object. A common pitfall is forgetting that async functions always return a Promise, so the rejected state must be caught by the caller. Using a discriminated union for the result can make error handling more explicit.
Reference solutiontypescript interface User { id: number; name: string; } async function fetchUser(id: number): Promise<User> { try { const response = await fetch(`https://api.example.com/users/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const user: User = await response.json(); return user; } catch (error) { console.error('Failed to fetch user:', error); throw error; // or return a fallback } } // Using the function: fetchUser(1) .then(user => console.log(user.name)) .catch(err => console.error(err)); // Time complexity: O(1) + network latency // Space complexity: O(1) - Design a type-safe event emitter class that enforces event names and payload types.What a strong answer covers
- Use a generic type parameter constrained to a union of event names.
- Store callbacks in a Map with event-specific payload types.
- Typed emit and on methods using keyof and conditional types.
View a sample answer
A type-safe event emitter ensures that each event name is associated with a specific payload type. Define an interface mapping event names to their payloads. The class uses a generic type parameter for the event map, and the methods 'on' and 'emit' are typed to accept only valid event names and corresponding payloads. Internally, store callbacks in a Map where keys are event names and values are arrays of listeners. A pitfall is that the 'on' method must capture the event-specific payload type, which requires using a function overload or conditional types. The implementation also needs to handle listener removal and memory leaks.
Reference solutiontypescript type EventMap = { [event: string]: (...args: any[]) => void; }; class TypedEmitter<T extends EventMap> { private listeners = new Map<keyof T, Set<(...args: any[]) => void>>(); on<K extends keyof T>(event: K, callback: T[K]): void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(callback); } emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void { const cbs = this.listeners.get(event); if (cbs) { cbs.forEach(cb => cb(...args)); } } off<K extends keyof T>(event: K, callback: T[K]): void { this.listeners.get(event)?.delete(callback); } } // Usage: interface MyEvents { data: (payload: { id: number }) => void; error: (message: string) => void; } const emitter = new TypedEmitter<MyEvents>(); emitter.on('data', ({ id }) => console.log(id)); emitter.emit('data', { id: 1 }); // Time complexity: O(1) for on/emit/off average // Space complexity: O(n) where n is number of listeners - Explain how `keyof` and `typeof` operators work. Provide examples of using them to create dynamic types.What a strong answer covers
- keyof T yields a union of property keys of T.
- typeof x returns the type of a variable x (value to type).
- Combine them to create dynamic types like keyof typeof obj.
View a sample answer
keyof is an operator that takes an object type and returns a union of its known, public property names. For example, keyof {a: number, b: string} yields 'a' | 'b'. typeof is used in type contexts to get the type of a value or variable; it is often used to derive a type from an existing object literal. They are frequently combined: e.g., typeof obj returns the type of the object, and keyof typeof obj gives the keys of that object's type. This allows you to create types that are tightly coupled to runtime values, such as typed Object.keys or dynamic property accessors. A common pitfall is using typeof on a type rather than a value, which yields 'string' or 'number' as a string literal, not the expected type.
- Use mapped types to create a `FlagProperties<T>` type that converts all boolean properties of an object type `T` to string literal 'true' | 'false'.What a strong answer covers
- Use mapped type with a conditional to check if property type is boolean.
- The result property type becomes 'true' | 'false' string literal.
- Handle optional properties correctly with key remapping.
View a sample answer
FlagProperties<T> uses a mapped type that iterates over the keys of T. For each property, it checks if T[P] extends boolean. If true, the property type is replaced with the union 'true' | 'false' (as string literals). If not, the original type is preserved. This is done using a conditional type inside the mapped type. Optional properties (?) are preserved using the key remapping syntax. A nuance is that boolean is a union of true | false, so extending boolean correctly captures both. However, beware that properties of type 'boolean' (the primitive) are captured, but literal types like true themselves also extend boolean. The resulting type cannot be assigned back to the original type without a cast.
Reference solutiontypescript type FlagProperties<T> = { [P in keyof T]: T[P] extends boolean ? 'true' | 'false' : T[P]; }; // Example: interface Config { debug: boolean; readonly: boolean; name: string; } // Result type: /* type FlaggedConfig = { debug: 'true' | 'false'; readonly: 'true' | 'false'; name: string; } */ // Time complexity: O(n) where n is number of properties // Space complexity: O(n)
How to prepare
- Practice by implementing utility types from scratch (e.g., Partial, Pick, ReturnType).
- Understand structural typing (duck typing) and how it affects type compatibility.
- Master advanced patterns like discriminated unions and branded types.
- Use the TypeScript playground and official challenges (e.g., type challenges) to refine your skills.
- Review real-world TypeScript codebases (e.g., React or Node.js libraries) to see patterns in action.
Frequently asked questions
Do I need to know TypeScript for a React job?
Yes, because most modern React codebases use TypeScript to catch errors early and improve developer experience.
How can I practice TypeScript for interviews?
Use the TypeScript playground, solve type challenges on GitHub (e.g., type-challenges), and implement small projects.
What are common mistakes in TypeScript interviews?
Overusing `any`, ignoring strict mode, and not understanding how conditional types work.
Is TypeScript difficult to learn?
Basics are easy, but advanced features like conditional types and template literal types require practice.
Should I memorize type definitions?
Focus on understanding concepts rather than memorizing. Knowing when to use generics or utility types is more important.
Practice TypeScript questions with instant AI feedback
Upload your resume, get a personalized mock interview, and see exactly what to improve — free to start.