Typescript interview questions and answers for 2025
TypeScript Interview Questions for Freshers and Intermediate Levels
What is TypeScript, and how does it improve upon JavaScript?
TypeScript is a superset of JavaScript that adds static typing and additional features to improve code quality and maintainability. It compiles down to plain JavaScript, making it compatible with any JavaScript environment.
How TypeScript Improves Upon JavaScript:
- Static Typing – Detects type errors at compile time, reducing runtime bugs.
- Better Code Readability & Maintainability – Enforces strict type rules, making the code easier to understand.
- Enhanced IDE Support – Provides autocompletion, refactoring tools, and better error detection in modern editors like VS Code.
- Interfaces & Generics – Helps create reusable and scalable code.
- Improved ES6+ Support – Supports modern JavaScript features with backward compatibility.
By using TypeScript, developers get safer, more scalable, and more maintainable code while still leveraging the flexibility of JavaScript.
What is type inference in TypeScript?
Type inference is TypeScript’s ability to automatically determine a variable’s type without explicit type annotations. It enhances code readability while ensuring type safety.
How Type Inference Works
- Basic InferenceTypeScript infers (determines) types based on assigned values:
// no need for type definition for the below lines let message = "Hello"; // Inferred as string let count = 42; // Inferred as number
- Function Return Type InferenceIf a function returns a value, TypeScript infers the return type:
function add(a: number, b: number) { return a + b; // Inferred as number }
- Inference in Arrays and Objects
let numbers = [1, 2, 3]; // Inferred as number[] let user = { name: "Alice", age: 30 }; // Inferred as { name: string, age: number }
Why It’s Useful
- Reduces boilerplate (no need to manually declare types).
- Improves developer experience with better autocompletion.
- Ensures type safety while keeping code concise.
Even with inference, explicit types may be needed in complex scenarios for better clarity and maintainability.
What are interfaces in TypeScript, and how do they differ from type aliases?
An interface in TypeScript defines the structure of an object, specifying its properties and types. It is mainly used for object shape enforcement and supports extensibility.
interface User {
name: string;
age: number;
}
const user: User = { name: "Alice", age: 25 }; // ✅ Valid
The key differences to type aliaser are:
- Interfaces are extendable using
extends
, while types use intersections (&
). - Interfaces support declaration merging, whereas types don’t.
- Types can represent other structures (like unions and tuples), while interfaces focus on object shapes.
What’s actually the declaration merging?
// Initial declaration of User
interface User {
name: string;
}
// Later in the code
interface User {
age: number;
}
// TypeScript merges these to:
// interface User {
// name: string;
// age: number;
// }
// With types, this fails:
type Animal = {
name: string;
}
type Animal = { // Error: Duplicate identifier 'Animal'
breed: string;
}
How do you define and use optional properties in TypeScript?
Optional properties are defined using a ?
after the property name in an interface or type.
interface User {
name: string;
age?: number; // Optional property
}
Using optional properties
const user1: User = { name: "Alice" }; // ✅ Valid (age is optional)
const user2: User = { name: "Bob", age: 30 }; // ✅ Valid (age is included)
Checking for optional properties
Since optional properties may be undefined
, we need to use conditional checks:
if (user1.age !== undefined) {
console.log(`User's age is ${user1.age}`);
}
Why use optional properties?
- They make objects definitions more flexible.
- We avoid unnecessary default values.
- This helps a lot with partial data structures, such as API responses.
Best Practice: Always check for undefined
before accessing an optional property to prevent runtime errors.
What is the difference between any and unknown in TypeScript?
Both of the types allow for storing any type of value, but they behave differently in terms of type safety.
any
is considered unsafe, and should be avoid when possible
- Disables all type checking, allowing any (nomen omen) operations on the value.
- Can lead to runtime errors due to unchecked operations.
let value: any;
value = "Hello";
value = 42;
value.toUpperCase(); // ✅ No error, but might fail at runtime
unknown
is the safer alternative
- Accepts any type but does not allow arbitrary operations without type checks.
- Enforces type checking before usage, making it safer.
let value: unknown;
value = "Hello";
// Error: Property 'toUpperCase' does not exist on type 'unknown'
value.toUpperCase();
// Safe: Type check required before usage
if (typeof value === "string") {
console.log(value.toUpperCase());
}
When to use what?
- Use
unknown
when accepting dynamic values but still enforcing type safety before usage. - Use
any
only as a last resort, as it bypasses TypeScript’s safety.
How do readonly properties work in TypeScript?
The readonly
keyword in TypeScript prevents reassignment of properties after initialization, ensuring immutability.
Defining readonly
Properties
interface User {
readonly id: number;
name: string;
}
const user: User = { id: 1, name: "Alice" };
// This is allowed
user.name = "Bob";
// Will throw: Cannot assign to 'id' because it is a readonly property
user.id = 2;
Readonly with classes
class Car {
readonly brand: string;
constructor(brand: string) {
this.brand = brand; // Allowed in constructor
}
}
const myCar = new Car("Toyota");
myCar.brand = "Honda"; // Error: Cannot assign to 'brand'
Key Points
readonly
prevents reassignment after initialization.- Allowed in constructor but not elsewhere.
- Useful for constants and immutable data.
What is a union type, and how does it work in TypeScript?
A union type in TypeScript allows a variable to hold multiple possible types, increasing flexibility while maintaining type safety. It is defined using the |
(pipe) operator.
Defining a union type
let value: string | number;
value = "Hello"; // Allowed
value = 42; // Allowed
value = true; // Error: Type 'boolean' is not assignable
Using union types in functions
function formatInput(input: string | number): string {
return typeof input === "number" ? input.toFixed(2) : input.toUpperCase();
}
console.log(formatInput(10)); // "10.00"
console.log(formatInput("hello")); // "HELLO"
Key Points
- Enables variables to accept multiple types.
- Requires type narrowing (e.g.,
typeof
checks) before using specific operations. - Useful for handling flexible data inputs (e.g., API responses).
How does TypeScript handle function return types?
TypeScript allows specifying a function’s return type to ensure type safety. If not explicitly defined, TypeScript infers the return type based on the function’s logic.
Explicit return type
function add(a: number, b: number): number {
return a + b; // TypeScript ensures the return value is a number
}
Inferred return type
function greet(name: string) {
return `Hello, ${name}`; // Inferred as string
}
Void return type (for functions that do not return any value)
function logMessage(message: string): void {
console.log(message);
}
Union return type (for functions that return different types)
function getValue(flag: boolean): string | number {
return flag ? "Success" : 0;
}
Key points
- Explicit return types improve readability and prevent unintended returns.
- Type inference works automatically but can be overridden.
- Use
void
for functions that don’t return a value. - Use union types for multiple possible return types.
What are generics in TypeScript, and why are they useful?
Generics in TypeScript allow us to create reusable code components that work with multiple types while maintaining type safety. They enable flexibility without sacrificing strong typing.
Basic generic example
function identity<T>(value: T): T {
return value;
}
console.log(identity<string>("Hello")); // "Hello"
console.log(identity<number>(42)); // 42
Here, <T>
is a type parameter that gets replaced with an actual type when the function is used.
Generics with arrays
function getFirst<T>(arr: T[]): T {
return arr[0];
}
console.log(getFirst<number>([1, 2, 3])); // 1
console.log(getFirst<string>(["a", "b"])); // "a"
Generics in interfaces
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "TypeScript" };
const numberBox: Box<number> = { value: 100 };
Why are generics useful?
- Code Reusability – Write functions and components that work with multiple types.
- Type Safety – Ensure correct types while maintaining flexibility across different data types.
- Better Readability – Reduce the need for multiple function overloads.
How does TypeScript support tuple types, and when would you use them?
A tuple in TypeScript is a fixed-length array where each element has a specific type. It allows storing multiple values of different types in a defined order.
Defining a tuple
let person: [string, number];
person = ["Alice", 30]; // Valid
person = [30, "Alice"]; // Error: Type mismatch
Tuple with optional and readonly elements
let user: [string, number?] = ["Bob"]; // ✅ Second element is optional
const coordinates: readonly [number, number] = [10, 20]; // ✅ Read-only tuple
Using tuples in functions
function getUser(): [string, number] {
return ["Charlie", 25];
}
When to use tuples?
- When order matters, like representing coordinates
[x, y]
or RGB colors[r, g, b]
. - Returning multiple values from a function with different types (good example could be the
useState
hook in React). - Improving readability over basic arrays when elements serve distinct roles.
How does method overloading work in TypeScript?
Method overloading in TypeScript allows a function to have multiple call signatures, enabling different input types while maintaining type safety.
Defining function overloads
You declare multiple function signatures, followed by a single implementation.
function transform(val: number): number;
function transform(val: string): string[];
function transform(val: number | string): number | string[] {
if (typeof val === "number") {
return val * 2; // Returns a number when input is number
} else {
return val.split(""); // Returns string array when input is string
}
}
// With overloads, TypeScript knows the precise return type based on input
const num = transform(10); // TypeScript knows: number
const arr = transform("hello"); // TypeScript knows: string[]
// Type-specific operations are now type-safe
const doubled = num + 5; // Works because TypeScript knows num is a number
const firstChar = arr[0]; // Works because TypeScript knows arr is an array
Key rules of overloading
- Only one function implementation (the last definition).
- The function implementation must handle all overload cases.
- Helps provide better type safety and flexibility.
When to use method overloading?
- When a function needs to handle different input types with unique behaviors.
- When working with flexible APIs (e.g., handling both
string
andnumber
inputs).
What is a discriminated union, and how does it improve type safety?
A discriminated union is a TypeScript pattern that combines union types with a common discriminant property to enable type-safe handling of multiple object shapes.
Defining a discriminated union
Each type in the union has a unique literal property (the discriminant), making type checking easier.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.side ** 2;
}
}
console.log(getArea({ kind: "circle", radius: 10 })); // 314.16
console.log(getArea({ kind: "square", side: 5 })); // 25
How it improves type safety
- Ensures all cases are handled, reducing runtime errors.
- Allows TypeScript’s type narrowing to infer the correct object type.
- Prevents invalid property access (e.g.,
shape.radius
on a square).
How do mapped types work in TypeScript?
Mapped types in TypeScript allow you to create new types by transforming properties of an existing type dynamically. They are useful for modifying, making properties optional, readonly, or changing their types.
Basic mapped type
type User = { name: string; age: number };
type OptionalUser = { [K in keyof User]?: User[K] };
// Equivalent to: { name?: string; age?: number; }
Using Readonly
, Partial
, and Required
TypeScript provides built-in mapped types:
type ReadonlyUser = Readonly<User>; // { readonly name: string; readonly age: number; }
type PartialUser = Partial<User>; // { name?: string; age?: number; }
type RequiredUser = Required<PartialUser>; // { name: string; age: number; }
Mapping with type transformation
type Stringified<T> = { [K in keyof T]: string };
type UserString = Stringified<User>; // { name: string; age: string; }
Why use mapped types?
- Helps in creating dynamic, reusable types.
- Reduces code duplication when modifying existing types.
- Improves type safety in complex object structures.
What is never in TypeScript, and when should it be used?
In TypeScript, never
represents a type that never has a value. It is used for functions that never return or for impossible code paths.
When never
is used
A) Functions that never return (e.g., throwing errors or infinite loops)
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
B. Exhaustiveness checking in switch statements
type Status = "success" | "error";
function handleStatus(status: Status) {
switch (status) {
case "success":
console.log("Operation succeeded");
break;
case "error":
console.log("Operation failed");
break;
default:
const _exhaustiveCheck: never = status;
// ❌ Compiler error if a case is missing
}
}
Why use never
?
- Ensures type safety by catching unhandled cases.
- Helps avoid logic errors in exhaustive condition checks.
- Prevents unintended return values in functions that should always throw errors or loop indefinitely.
How do keyof and typeof help with type operations in TypeScript?
keyof
and typeof
are TypeScript utilities that enhance type safety and dynamically infer types.
The keyof
operator returns a union of all property keys in an object type.
type User = { name: string; age: number };
type UserKeys = keyof User; // "name" | "age"
let key: UserKeys = "name"; // Valid
key = "age"; // Valid
key = "email"; // Error: "email" is not a key in User
The typeof
operator extracts the type of a variable or object, making it reusable.
const user = { name: "Alice", age: 25 };
type UserType = typeof user; // { name: string; age: number }
Combining keyof
and typeof
for type-safe access
const HttpStatusCodes = {
OK: 200,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500
} as const;
// Using typeof to get the type of the object,
// then keyof to get a union of its keys
type StatusCodeKeys = keyof typeof HttpStatusCodes;
// StatusCodeKeys is: "OK" | "NOT_FOUND" | "INTERNAL_SERVER_ERROR"
// Function that accepts only valid status code keys
function getStatusMessage(statusKey: StatusCodeKeys): string {
const code = HttpStatusCodes[statusKey];
return `HTTP ${code}: ${statusKey.replace('_', ' ')}`;
}
// Type-safe usage:
getStatusMessage("OK"); // Works
getStatusMessage("NOT_FOUND"); // Works
getStatusMessage("FORBIDDEN"); // Error: Argument of type '"FORBIDDEN"' is not assignable to parameter of type 'StatusCodeKeys'
Why use keyof
and typeof
?
keyof
ensures only valid object keys are used.typeof
allows dynamic type inference, avoiding manual type definitions.
What is type assertion, and when should you use it?
Type assertion in TypeScript tells the compiler to treat a value as a specific type when TypeScript cannot infer it correctly. It does not change the actual value at runtime—only how TypeScript interprets it.
Syntax for type assertion
There are two ways to assert a type:
let value: unknown = "Hello, TypeScript";
// Method 1: Using "as" syntax
let strLength: number = (value as string).length;
// Method 2: Using angle-bracket syntax
let strLength2: number = (<string>value).length;
When to use type assertions?
- When TypeScript cannot infer the type correctly (e.g., working with
unknown
orany
). - When handling DOM elements in a TypeScript-based frontend project:
const input = document.getElementById("username") as HTMLInputElement; console.log(input.value);
- When working with API responses where TypeScript does not know the structure beforehand.
When to avoid type assertions?
- If TypeScript already knows the type, assertions are unnecessary.
- Avoid forcing an incorrect type, as it can cause runtime errors.
What is the difference between extends and implements in TypeScript?
Both extends
and implements
help with inheritance, but they serve different purposes.
extends
– is used for inheriting from a Class or Interface
A class can extend another class to inherit properties/methods:
class Animal {
move() { console.log("Moving"); }
}
class Dog extends Animal {
bark() { console.log("Barking"); }
}
const dog = new Dog();
dog.move(); // Inherited from Animal
dog.bark(); // Defined in Dog
An interface can extend another interface:
interface Person {
name: string;
}
interface Employee extends Person {
salary: number;
}
const emp: Employee = { name: "Alice", salary: 5000 };
implements
– helps enforcing an interface in a Class
Used when a class must follow the structure of an interface but does not inherit behavior.
interface Printable {
print(): void;
}
class Book implements Printable {
print() { console.log("Printing book..."); }
}
When to use what?
- Use
extends
when inheriting behavior from a class or extending an interface. - Use
implements
when forcing a class to follow a specific contract.
What is TypeScript’s strict mode, and why is it recommended?
Strict mode in TypeScript enables a set of strict type-checking rules that improve code safety and maintainability. It is activated by setting "strict": true
in tsconfig.json
:
Key features of strict
mode:
strictNullChecks
– Preventsnull
orundefined
from being assigned to non-nullable types.strictPropertyInitialization
– Ensures all class properties are initialized before use.noImplicitAny
– Requires explicit type annotations instead of allowingany
by default.noImplicitReturns
– Ensures functions always return a value in all code paths.noUncheckedIndexedAccess
– Requires checking if an array or object property exists before accessing it.
Why use strict
mode?
- Prevents runtime errors by catching potential issues early.
- Enforces better coding practices and makes refactoring safer.
- Improves maintainability by ensuring type correctness.
It’s best to always enable strict mode in production projects to maximize type safety and code reliability.
What are declaration files (.d.ts), and why are they useful?
TypeScript declaration files are files that provide type information for JavaScript code. They’re essentially type definitions without actual implementation code.
These files are useful because they allow TypeScript to understand the types of JavaScript libraries that weren’t written in TypeScript originally. For example, if you’re using a library like Lodash or jQuery, TypeScript wouldn’t know the types of functions and objects these libraries provide. Declaration files solve this problem.
They can also be an output of TypeScript compilation. When TypeScript code is compiled to JavaScript, declaration files can be generated (using --declaration
flag) to preserve type information, enabling other TypeScript projects to consume the compiled JavaScript while maintaining full type safety.
The main benefits of declaration files are:
- They enable autocompletion in the code editor
- They provide type checking for external JavaScript code
- They help catch errors at compile time rather than runtime
Most popular libraries have declaration files available through the DefinitelyTyped repository. We can install them using npm or yarn with the @types/
prefix.
How do you ensure type safety when working with external APIs?
When working with external APIs, we may often face the problem that responses are untyped JSON data, which can lead to various errors. TypeScript helps ensure type safety through proper, manual typing and validation.
Here are two options, how to deal with the problem:
- Define API response typesCreate an interface to describe the expected data structure:
interface User { id: number; name: string; email: string; }
Then use type casting for fetched data:
async function getUser(id: number): Promise<User> { const response = await fetch(`https://api.example.com/users/${id}`); const data = await response.json(); // Type assertion (casting) using the "as" keyword return data as User; }
- Use type guards for validation
function isUser(obj: any): obj is User { return typeof obj.id === "number" && typeof obj.name === "string"; } async function fetchUser(id: number) { const data = await getUser(id); if (isUser(data)) { console.log(data.name); // Type-safe } else { console.error("Invalid API response"); } }
How do you use as const in TypeScript, and why is it helpful?
The as const
assertion in TypeScript converts a value into a readonly type, preventing modifications and enabling strict type inference.
Using as const
for immutable values
const STATUS = {
success: "SUCCESS",
error: "ERROR"
} as const;
STATUS.success = "FAILED"; // ❌ Error: Cannot modify readonly property
Here, STATUS.success
is inferred as "SUCCESS"
instead of a general string
.
Using as const
with arrays:
const directions = ["up", "down", "left", "right"] as const;
directions.push("forward"); // ❌ Error: Readonly tuple
Now, directions
is treated as a readonly tuple, ensuring fixed values.
This helps enforce strict value constraints.
Why is as const
helpful?
- Ensures values remain unchanged.
- Improves type inference, avoiding unnecessary widening to
string
ornumber
. - Useful for constants, enums, and fixed-value arrays.
TypeScript Interview Questions for Experienced Levels
How do template literal types improve type safety in TypeScript?
Template literal types in TypeScript allow us to create dynamically generated string types based on template literals (similar to JavaScript template strings). They help enforce strict typing for string-based values like keys, routes, and commands.
Basic usage
type Status = "success" | "error";
type StatusMessage = `status-${Status}`;
let msg: StatusMessage = "status-success"; // Valid
msg = "status-failed"; // ❌ Error: Type '"status-failed"' is not assignable
Here, StatusMessage
can only be "status-success"
or "status-error"
, preventing invalid values.
Enforcing API route patterns
type Route = "home" | "profile" | "settings";
type APIEndpoint = `/api/${Route}`;
let endpoint: APIEndpoint = "/api/home"; // Valid
endpoint = "/api/logout"; // ❌ Error: Not a valid route
This ensures that developers cannot use invalid API endpoints, reducing runtime errors.
Combining with generics for dynamic string constraints
type EventType<T extends string> = `event-${T}`;
let clickEvent: EventType<"click"> = "event-click"; // Valid
Why use template literal types?
- Prevents invalid string assignments by enforcing strict typing.
- Improves autocomplete suggestions in IDEs.
- Helps with API consistency, route handling, and event naming.
How do index signatures work, and when should they be used cautiously?
An index signature in TypeScript allows defining an object type with dynamic keys where all values share the same type. It is useful when object properties are unknown in advance.
Defining an index signature
interface UserRoles {
[key: string]: boolean;
}
const roles: UserRoles = {
admin: true,
editor: false,
guest: true,
};
Here, roles
can have any string key, but all values must be boolean
.
Restricting key types
We can limit key types to improve type safety:
interface RolePermissions {
[key: "admin" | "editor" | "viewer"]: boolean;
}
const permissions: RolePermissions = { admin: true, editor: false }; // Valid
When to use index signatures cautiously?
- No type safety on keys: index signatures allow any key, even unintended ones
console.log(roles.unknownKey); // No error, but returns `undefined`
- Loss of specific property types:If you declare an index signature with a strict type, like
[key: string]: string
, then every property (even explicitly defined ones) must conform to that type.This can be problematic because it prevents you from having properties with more complex or different types.Example: stricter index signature (string
) causes restrictionsinterface Config { theme: string; [key: string]: string; } // TypeScript forces `theme` to be a string (ok), // but prevents adding properties with different types later. const config: Config = { theme: "dark", mode: "auto" // ok // If you tried to add a number, it would cause an error: version: 1, // Error: number is not assignable to string };
For flexible index signature use
unknown
as this avoids restrictions:interface APIError { message?: string; [key: string]: unknown; } // Properties can have any type without conflict. const error: APIError = { message: "Something went wrong", code: 500, // number details: { timeout: true }, // object };
How does TypeScript handle type narrowing, and how does it improve type safety?
Type narrowing in TypeScript is the process of refining a broader type into a more specific type based on runtime checks. This ensures safer operations by allowing only valid properties or methods to be accessed.
Using typeof
for primitive type narrowing
function processValue(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // Allowed (TypeScript knows value is a string)
} else {
return value.toFixed(2); // Allowed (TypeScript knows value is a number)
}
}
Using instanceof
for object type narrowing
class Dog {
bark() { console.log("Woof!"); }
}
class Cat {
meow() { console.log("Meow!"); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // Safe access
} else {
animal.meow(); // Safe access
}
}
Using in
for property-based narrowing
type Car = { brand: string; speed: number };
type Bicycle = { brand: string; gearCount: number };
function describe(vehicle: Car | Bicycle) {
if ("speed" in vehicle) {
console.log(`This car runs at ${vehicle.speed} km/h`);
} else {
console.log(`This bicycle has ${vehicle.gearCount} gears`);
}
}
How does it improves type safety?
- Prevents accessing invalid properties on union types.
- Ensures only valid operations are performed on a value.
- Reduces runtime errors by catching type mismatches at compile time.
What is conditional typing, and how is it useful for function signatures?
Conditional typing in TypeScript allows defining types that change based on conditions using the extends
keyword. It helps create flexible, type-safe function signatures by adapting return types based on input types.
Basic conditional type syntax
type IsString<T> = T extends string ? "It's a string" : "Not a string";
type Test1 = IsString<string>; // "It's a string"
type Test2 = IsString<number>; // "Not a string"
Using conditional types in function signatures
Conditional typing helps create functions that return different types based on input.
function processInput<T>(input: T): T extends string ? string[] : number {
return (typeof input === "string" ? input.split("") : 0) as any;
}
const result1 = processInput("hello"); // Type: string[]
const result2 = processInput(42); // Type: number
The as any
part bypasses TypeScript’s type checking for the return value, essentially saying “trust me, I know what I’m doing.” After that, TypeScript will infer the correct type based on the conditional return type you specified.
Restricting function arguments dynamically
type AllowedType<T> = T extends "admin" ? boolean : string;
function getConfig<T extends "admin" | "user">(role: T): AllowedType<T> {
return (role === "admin" ? true : "User settings") as AllowedType<T>;
}
const adminConfig = getConfig("admin"); // boolean
const userConfig = getConfig("user"); // string
Why use conditional typing?
- Enforces stricter type safety by tailoring return types.
- Avoids unnecessary function overloading for different input types.
- Improves function flexibility while maintaining strong typing.
How do distributive conditional types work, and when are they useful?
Distributive conditional types happen when a generic passed for the conditional typing is given a union — TypeScript automatically applies the conditional type to each member of the union individually, instead of the union as a whole. This helps create more flexible and reusable type transformations.
Basic syntax of distributive conditional types
When a conditional type operates on a union, TypeScript distributes the condition across each type individually:
type IsString<T> = T extends string ? "Yes" : "No";
type Result = IsString<string | number>;
// Result becomes the "Yes" | "No" union
Instead of checking string | number
as a whole, TypeScript evaluates:
IsString<string>
→"Yes"
IsString<number>
→"No"
Resulting in "Yes" | "No"
Example: extracting specific types from a union
type ExtractString<T> = T extends string ? T : never;
type StrOnly = ExtractString<string | number | boolean>;
// string (Filters out non-string types)
ExtractString<T>
is a distributive conditional type:string extends string ? string : never
→string
number extends string ? never
→never
boolean extends string ? never
→never
- So the final result becomes just
string
.
Example: making properties optional in a union
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// string[] | number[]
- Provided
T
isstring | number
- TypeScript distributes the conditional:
string extends any ? string[] : never
→string[]
number extends any ? number[] : never
→number[]
- Final result:
string[] | number[]
When are distributive conditional types useful?
- Filtering specific types from unions (
Extract<T, U>
andExclude<T, U>
use this principle). - Applying transformations to each union member (e.g., making all strings uppercase).
- Building utility types that dynamically adapt based on input types.
What is the difference between type assertion and type casting, and when should each be used?
Both type assertion and type casting are used to tell TypeScript to treat a value as a specific type, but they work differently.
Type assertion (as
or <type>
in TypeScript)
Type assertion tells TypeScript to trust the code author that a value is of a certain type, without changing the value itself.
let value: any = "Hello";
let strLength: number = (value as string).length; // ✅ TypeScript assumes value is a string
Use when you know the type better than TypeScript (e.g., working with DOM elements or API responses).
Type casting (JavaScript concept, not TypeScript)
Type casting converts a value into another type at runtime. Since TypeScript doesn’t change types at runtime, we must use JavaScript functions like String()
, Number()
, or Boolean()
.
let numStr: string = "42";
let num: number = Number(numStr); // Converts string to number
Use when actual type conversion is needed (e.g., converting string
to number
).
When to use which?
- Use Type Assertion when TypeScript fails to infer a known type, but you are sure of its structure.
- Use Type Casting when you need actual data conversion between types at runtime.
How does structural typing differ from nominal typing, and how does TypeScript handle it?
TypeScript follows a structural typing system, which differs from nominal typing used in languages like Java or C#.
What is structural typing?
Structural typing (also called duck typing) means that types are compatible based on their structure, not their explicit name.
How Does TypeScript Handle Structural Typing?
- Objects are compared based on their properties, not type names.
- Extra properties do not break compatibility, as long as required ones match.
- Classes also follow structural typing, unlike nominally-typed languages.
Basic example:
interface Person {
name: string;
age: number;
}
let user = { name: "Alice", age: 30, role: "admin" };
let person: Person = user; // Allowed (matches structure)
Since user
has at least the required properties, TypeScript allows the assignment, even though the object has extra properties.
Class similarity example:
class User {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
}
let emp: Employee = new User(); // Allowed in TypeScript (structural typing)
In nominal typing languages, User
and Employee
would be incompatible unless explicitly declared as related.
What is nominal typing?
Nominal typing requires explicit declarations for type compatibility—only types with the same name are considered compatible. TypeScript does not use nominal typing by default.
How can nominal typing be applied in TypeScript?
TypeScript allows nominal typing simulation using branded types:
// Define a branded type
type UserID = string & { readonly brand: unique symbol };
// Function that expects a UserID
function getUser(id: UserID) {
console.log(`Fetching user with ID: ${id}`);
}
// Correct usage
const rawId = "12345";
const userId = rawId as UserID; // Explicit type assertion required
getUser(userId); // OK
// Incorrect usage
const someString = "hello";
getUser(someString); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'UserID'
Even though UserID
is structurally a string
, it is treated as a distinct, nominal type.
How do you create generic constraints in TypeScript to restrict possible types?
In TypeScript, generic constraints ensure that a generic type meets specific requirements, preventing invalid types from being used.
Using extends
to enforce constraints
We can restrict a generic type to specific types or structures using extends
:
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
console.log(getLength("Hello")); // Allowed (string has length)
console.log(getLength([1, 2, 3])); // Allowed (array has length)
console.log(getLength(42)); // Error: 'number' has no 'length' property
Here, T extends { length: number }
ensures only types with a length
property are valid.
Constraining to a specific type
function getId<T extends string | number>(id: T) {
return `User ID: ${id}`;
}
console.log(getId(123)); // Allowed
console.log(getId("ABC123")); // Allowed
console.log(getId(true)); // Error: 'boolean' is not allowed
This restricts T
to either string
or number
, preventing unsupported types.
Constraining to a class or interface
interface Person {
name: string;
}
function greet<T extends Person>(person: T) {
console.log(`Hello, ${person.name}`);
}
greet({ name: "Alice" }); // Allowed
greet({ age: 30 }); // Error: 'name' property is missing
Only objects that match Person
(i.e., have a name
property) are allowed.
Why use generic constraints?
- Prevents invalid types from being used.
- Ensures type safety while keeping flexibility.
- Reduces runtime errors by enforcing structure at compile time.
What is the difference between Partial, Required, and Readonly<T>?
TypeScript provides utility types that modify object properties dynamically:
Partial<T>
– Makes all properties optional
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>;
const user: PartialUser = { name: "Alice" }; // 'age' is optional
Required<T>
– Makes all properties mandatory
type RequiredUser = Required<User>;
const user: RequiredUser = { name: "Alice" }; // Error: Missing 'age'
Readonly<T>
– Prevents property modification
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { name: "Alice", age: 30 };
user.age = 31; // Error: Cannot modify readonly property
How do you extract function argument types using Parameters<T>?
TypeScript’s Parameters<T>
utility type extracts the parameter types of a function as a tuple.
Basic usage
function greet(name: string, age: number) {
return `Hello, ${name}. You are ${age} years old.`;
}
type GreetParams = Parameters<typeof greet>;
// Equivalent to: type GreetParams = [string, number];
const args: GreetParams = ["Alice", 30]; // Valid
Using Parameters<T>
for dynamic typing
This is useful when handling higher-order functions or function decorators.
type Func = (id: number, active: boolean) => void;
type Args = Parameters<Func>; // Equivalent to: [number, boolean]
function callFunction(fn: Func, ...args: Args) {
return fn(...args);
}
Why Use Parameters<T>
?
- Extracts function argument types dynamically.
- Improves type safety in function utilities and decorators.
- Avoids manual duplication of parameter type definitions.
What is the difference between Exclude<T, U> and Extract<T, U>?
Both Exclude<T, U>
and Extract<T, U>
are utility types used to filter union types, but they serve opposite purposes.
Exclude<T, U>
– removes elements from T
that are assignable to U
.
type Status = "success" | "error" | "pending";
type Excluded = Exclude<Status, "error">;
// Equivalent to: "success" | "pending"
Use Case: When we need to remove unwanted values from a union type.
Extract<T, U>
– keeps only these elements from T
that are assignable to U
.
type Extracted = Extract<Status, "success" | "pending">;
// Equivalent to: "success" | "pending"
Use Case: When we need to pick specific values from a union type.
How can generic utility types help with API response handling?
Generic utility types like Partial
, Promise
, Pick
, Omit
, and Record
help describe transformations on API responses:
Using Partial<T>
for optional API fields
APIs may return or expect partial data, especially in update operations.
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
const updateUser = (user: PartialUser) => {
// API call allowing partial updates
};
Ensures only valid properties are included while keeping others optional.
Handling API errors with Promise<T>
and Pick<T, K>
type ApiResponse<T> = Promise<{ data: T; error?: string }>;
async function fetchUser(): ApiResponse<Pick<User, "id" | "name">> {
// do something...
return { data: { id: 1, name: "Alice" } };
}
Ensures the function returns a valid subset of User
, preventing over-fetching.
Using Readonly<T>
for immutable API data
async function checkUser(user: Readonly<User>) {
// do something...
user.name = "Bob"; // ❌ Error: Cannot modify readonly property
}
Prevents unintended modifications of fetched API data.
Why use generic utility types?
- Ensures correct API response structure dynamically.
- Prevents unnecessary data mutation for immutable API objects.
- Improves code maintainability by reducing type duplication.
How do you implement recursive types for handling deeply nested structures?
Recursive types are when a type refers to itself. This is useful for trees, nested objects, and hierarchical data.
Defining a recursive type for nested objects
interface NestedObject {
value: string;
children?: NestedObject[];
}
const data: NestedObject = {
value: "Root",
children: [
{ value: "Child 1" },
{ value: "Child 2", children: [{ value: "Grandchild" }] },
],
};
This allows for infinite nesting of child objects.
Recursive type for JSON-like structures
type JSONValue = string | number | boolean | JSONValue[] | { [key: string]: JSONValue };
const jsonData: JSONValue = {
name: "Alice",
age: 30,
single: true,
hobbies: ["reading", "gaming"],
skills: {
"dance": 4,
"languages": 5
}
};
Handles dynamic, nested API responses.
Using Recursive Types in Functions
function printValues(obj: NestedObject): void {
console.log(obj.value);
obj.children?.forEach(printValues);
}
printValues(data);
Traverses nested structures safely.
Why use recursive types?
- Handles deeply nested data (e.g., trees, JSON structures).
- Prevents type mismatches in nested objects.
- Improves maintainability by defining a single, scalable structure.
What is the best way to enforce type safety when working with dynamic objects?
TypeScript provides various ways to enforce type safety of objects of unknown/dynamic type:
Using index signatures for flexible key-value objects
If the keys are unknown but values follow a type pattern:
interface DynamicObject {
[key: string]: string | number;
}
const user: DynamicObject = { name: "Alice", age: 30 }; // Valid
Using generics for typed objects
For reusable structures with varying property types:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // Type-safe retrieval
Using Record<K, V>
for fixed key-value objects
If the keys are known but values vary:
type UserRoles = Record<"admin" | "editor" | "viewer", boolean>;
const roles: UserRoles = {
admin: true,
editor: false,
viewer: true
}; // Prevents adding extra properties
Validating dynamic objects with type guards
For API responses or external data, use type guards:
function isUser(obj: any): obj is { name: string; age: number } {
return typeof obj.name === "string" && typeof obj.age === "number";
}
const data = JSON.parse('{ "name": "Alice", "age": 30 }');
if (isUser(data)) console.log(data.name); // Safe access
Ensures runtime validation.
How do abstract classes work, and how do they compare to interfaces?
Abstract classes define a common structure and optional shared behavior for subclasses.
They cannot be instantiated directly and ensure that certain methods or properties are implemented by derived classes while allowing for additional properties and methods.
Defining an abstract class
abstract class Animal {
constructor(public name: string) {}
abstract makeSound(): void; // Must be implemented by subclasses
move(): void {
console.log(`${this.name} is moving`);
}
}
class Dog extends Animal {
makeSound() {
console.log("Woof!");
}
}
const dog = new Dog("Buddy");
dog.makeSound(); // "Woof!"
dog.move(); // Inherited from Animal
Abstract classes vs. interfaces
Feature | Abstract Class | Interface |
Can have method implementations? | Yes | No |
Can define properties? | Yes | Yes (only declarations) |
Supports multiple inheritance? | No | Yes (via multiple interfaces) |
Can be instantiated? | No | No |
When to use each?
- Use an abstract class when shared behavior (implemented methods) is needed.
- Use an interface when defining a contract without implementation.
How do private, protected, and public access modifiers affect class inheritance?
In TypeScript, access modifiers (private
, protected
, public
) define how and where class members can be accessed, especially across inheritance hierarchies.
public
– element is accessible everywhere
- this is a default visibility (if no modifier is specified).
- accessible inside the class, in subclasses, and outside the class.
class Animal {
public name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} says Woof!`); // accessible in subclass
}
}
const dog = new Dog("Buddy");
console.log(dog.name); // accessible outside
private
– makes element accessible only within the same class
- Not accessible in subclasses or outside the class.
class Animal {
private age: number;
constructor(age: number) {
this.age = age;
}
}
class Dog extends Animal {
showAge() {
console.log(this.age); // ❌ Error: 'age' is private
}
}
const dog = new Dog(5);
console.log(dog.age); // Error: 'age' is private
protected
– makes element accessible within the class and subclasses
- Not accessible outside the class
class Animal {
protected type: string;
constructor(type: string) {
this.type = type;
}
}
class Dog extends Animal {
showType() {
console.log(`This is a ${this.type}`); // Accessible inside subclass
}
}
const dog = new Dog("Mammal");
console.log(dog.type); // Error: 'type' is protected
What is a TypeScript mixin, and how does it compare to class inheritance?
A mixin in TypeScript is a pattern that allows reusing functionality across multiple classes without deep inheritance. Unlike traditional class inheritance, mixins compose behavior dynamically.
Defining a mixin in TypeScript
Mixins use higher-order functions to extend a class with additional behavior.
type Constructor<T = {}> = new (...args: any[]) => T;
function CanLog<T extends Constructor>(Base: T) {
return class extends Base {
log(message: string) {
console.log(`[LOG]: ${message}`);
}
};
}
class User {
constructor(public name: string) {}
}
const LoggingUser = CanLog(User);
const user = new LoggingUser("Alice");
user.log("User created!"); // [LOG]: User created!
Mixin vs. Class Inheritance
Feature | Mixins | Class Inheritance |
Code Reuse | Yes | Yes |
Multiple Inheritance | Yes (can mix multiple behaviors) | No (single inheritance) |
Tight Coupling | No (loosely coupled) | Yes (subclasses depend on the base class) |
Runtime Modification | Yes (dynamic behavior) | No (fixed hierarchy) |
When to use mixins?
- When multiple classes need shared behavior without a rigid hierarchy.
- When composition is preferable over deep inheritance.
- When adding optional features to existing classes.
How do decorators work in TypeScript, and how can they be used?
Decorators in TypeScript are special functions that modify classes, their methods, properties, or parameters at runtime. They are commonly used for metadata, logging, validation, and dependency injection.
Enabling decorators
Decorators must be enabled in tsconfig.json
using experimentalDecorators
flag.
Note: Newer versions of TypeScript (5.x) also provide “native” EcmaScript’s in-progress proposal for decorators. The TS team’s announcement delivers a note:
The --experimentalDecorators
flag will continue to exist for the foreseeable future; however, without the flag, decorators will now be valid syntax for all new code. Outside of --experimentalDecorators
, they will be type-checked and emitted differently. The type-checking rules and emit are sufficiently different that while decorators can be written to support both the old and new decorators behavior, any existing decorator functions are not likely to do so.
Basic class decorator example
A class decorator modifies a class at runtime:
function Logger(target: Function) {
console.log(`Class ${target.name} was created`);
}
@Logger
class User {
constructor(public name: string) {}
}
// Logs: "Class User was created"
Method decorator example
Method decorators wrap and modify methods dynamically.
function MeasureTime(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.time(key);
const result = originalMethod.apply(this, args);
console.timeEnd(key);
return result;
};
}
class MathOperations {
@MeasureTime
calculate() {
for (let i = 0; i < 1e6; i++) {
// Simulated work
}
}
}
new MathOperations().calculate(); // Logs execution time
Property decorator example
function Readonly(target: any, key: string) {
Object.defineProperty(target, key, { writable: false });
}
class Settings {
@Readonly
apiUrl = "<https://api.example.com>";
}
const config = new Settings();
config.apiUrl = "<https://new-api.com>"; // Error: Cannot modify readonly property
When to use decorators?
- Logging & Performance Tracking (e.g.,
@MeasureTime
) - Enforcing Rules (e.g.,
@Readonly
,@Required
etc.) - Dependency Injection (e.g.,
@Inject
in Angular) - Metadata Storage (e.g.,
Reflect.metadata
for frameworks)
How do you optimize TypeScript compilation speed in large projects?
As TypeScript projects grow, compilation can slow down. Optimizing tsconfig.json
, using incremental builds, and leveraging tools can improve performance.
Enable incremental compilation (incremental
)
Caches previous compilations, reducing recompile time.
{
"compilerOptions": {
"incremental": true
}
}
Use composite
mode for multi-project builds
For monorepos or multi-module projects, composite
mode compiles dependencies separately.
{
"compilerOptions": {
"composite": true
}
}
exclude
unnecessary files
Avoid compiling node_modules
and unnecessary files:
{
"exclude": ["node_modules", "dist", "coverage"]
}
Use skipLibCheck
to speed up type checking
Skips type checking for .d.ts
files:
{
"compilerOptions": {
"skipLibCheck": true
}
}
Speeds up compilation without affecting correctness in most cases. We’re making an assumption that the authors of the provided lib’s declarations have deeply checked them.
Run tsc --watch
instead of full compilation
Instead of recompiling everything, use the watch mode. This recompiles only changed files, reducing build times.
Use Babel for faster transpilation
For projects using only type checking, use Babel to transpile TypeScript faster:
npm install --save-dev @babel/preset-typescript
It’s faster than for JavaScript output, but skips type checking.
How does TypeScript’s type system impact JavaScript bundle size, and how can it be optimized?
TypeScript’s type system does not directly impact JavaScript bundle size because types are erased during compilation. However, indirect factors can lead to larger bundles.
How TypeScript can increase bundle size
- Excessive Importing: Including entire libraries instead of tree-shaking unused code. This is actually a generic problem. Regardless of use of TypeScript in our project.
- Unused (dead) code: Keeping unnecessary types or variables in compiled output.
- Decorators: use of
experimentalDecorators
flag emits extra metadata, increasing bundle size. - Large TypeScript Enums: Generates extra JavaScript code.
Optimizing TypeScript for smaller bundles
- Use
const enum
Instead ofenum
const enum Status { Success, Error, }
const enum
removes extra JavaScript runtime objects - Enable tree shaking with ES modulesUse
"module": "ESNext"
intsconfig.json
. This helps remove unused imports, reducing bundle size. - Avoid unnecessary decorator metadata
{ "compilerOptions": { "emitDecoratorMetadata": false } }
- Minimize runtime helpers with
tslib
Install tslib and use"importHelpers": true
to reduce redundant helper functions in each file. - Use
ES2017
orESNext
target: as this allows TypeScript to skip adding helper functions for many features existing in these standards. - Use smaller librariesInstead of importing entire libraries, use direct imports. Seek for smaller alternatives.
How do you type React hooks like useState and useReducer correctly?
TypeScript enforces type consistency for React state management, preventing errors by ensuring state values match their declared types throughout component lifecycles.
Typing useState
Explicitly define the state type:
import { useState } from "react";
// Ensures count is always a `number`.
const [count, setCount] = useState<number>(0);
setCount(5); // Allowed
setCount("hello"); // Error: Type 'string' is not assignable to type 'number'
Typing for complex state objects:
interface User {
name: string;
age: number;
}
// Ensures **user object** has the required structure
const [user, setUser] = useState<User | null>(null);
setUser({ name: "Alice", age: 30 }); // Allowed
setUser(null); // Allowed due to 'User | null'
Typing useReducer
Define state and action types:
import { useReducer } from "react";
// Define action types as constants
const ActionTypes = {
INCREMENT: 'increment',
DECREMENT: 'decrement'
} as const;
// Create a type from the values of ActionTypes
type ActionType = typeof ActionTypes[keyof typeof ActionTypes];
type State = { count: number };
type Action = { type: ActionType };
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionTypes.INCREMENT:
return { count: state.count + 1 };
case ActionTypes.DECREMENT:
return { count: state.count - 1 };
default:
return state;
}
}
// in the component
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "increment" }); // Allowed; also with ActionTypes.INCREMENT
dispatch({ type: "reset" }); // Error: Type '"reset"' is not assignable to type ActionType
How does TypeScript improve maintainability in large-scale React applications?
TypeScript enhances code maintainability in large React applications mainly by improving developer experience, and enforcing consistent data structures.
- Strongly typed props and stateTypeScript ensures components receive the correct props:
interface UserProps { name: string; age: number; } const UserCard: React.FC<UserProps> = ({ name, age }) => ( <div>{name} is {age} years old.</div> ); <UserCard name="Alice" age={30} />; // Correct <UserCard name="Alice" age="30" />; // Error: 'age' must be a number
Prevents incorrect prop usage at compile time.
- Improved code refactoringTypeScript helps catch errors when renaming or modifying components.Ensures function parameters match expected types, reducing unintended side effects.
function greetUser(user: { name: string; age: number }) { return `Hello, ${user.name}`; } // Refactoring 'name' to 'fullName' triggers TypeScript errors, preventing unnoticed bugs.
- Better hook type safetyPrevents unintended state mutations:
const [count, setCount] = useState<number>(0); setCount("ten"); // Error: Argument of type 'string' is not assignable to type 'number'
- Safer API calls and global state managementHelps define strict API response structures:
interface User { id: number; name: string; } async function fetchUser(): Promise<User> { const response = await fetch("/api/user"); return response.json(); // Ensures correct response type }
- Easier debugging and better IDE supportTypeScript provides autocomplete, inline documentation, and type hints, making it easier to navigate and maintain code.Prevents issues like undefined function parameters or invalid prop types.
What are the challenges of using TypeScript with third-party JavaScript libraries, and how can they be resolved?
When using JavaScript libraries in a TypeScript project, type safety issues can arise if the library lacks TypeScript support. Below are common challenges and ways to resolve them.
Missing type definitions (for external libraries)
- Problem: Some JavaScript libraries don’t provide TypeScript type definitions.
- Solution: Check for community-maintained types in DefinitelyTyped:
npm install --save-dev @types/some-library
If available, this adds type definitions automatically.
No official type definitions
- Problem: If no
@types/
package exists, TypeScript throws errors. - Solution: Create a custom type declaration file:
// types/global.d.ts declare module "some-library" { export function someFunction(): void; }
Place it in a
types/
folder and ensuretsconfig.json
includes it.
Incorrect or incomplete type definitions
- Problem: Some libraries ship with outdated or incorrect types.
- Solution: Use module augmentation to fix the types:
// Fix incorrect types declare module "some-library" { interface SomeType { newProperty: string; } }
Extends and overrides faulty type definitions.
Dynamic imports and require()
usage
- Problem: Some JavaScript libraries don’t use ES modules, requiring
require()
. - Solution: Use
import * as
ordefault
imports cautiously:import * as library from "some-library"; // Works for CommonJS import library from "some-library"; // Works if default export exists
Using any
as a last resort
- Problem: When all else fails, TypeScript won’t recognize the library.
- Solution: Use
any
orunknown
temporarily:// Less safe const lib: any = require("some-library"); // Safer but requires type checks const lib: unknown = require("some-library");
TypeScript Coding Interview Questions
Define a generic function that takes an array and returns its first element.
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
// Example usage:
console.log(getFirstElement([1, 2, 3])); // Output: 1
console.log(getFirstElement(["a", "b", "c"])); // Output: "a"
console.log(getFirstElement([])); // Output: undefined
Explanation
- Generic type
<T>
– Allows the function to work with any array type (number[]
,string[]
, etc.). - Return type
T | undefined
– Indicates the function returns either an element of typeT
orundefined
(when accessing an empty array’s first element). - Ensures Type Safety – The returned value will always match the type of elements in the array.
Create a type-safe function that only accepts objects containing a name: string property.
function printName<T extends { name: string }>(obj: T): void {
console.log(`Name: ${obj.name}`);
}
// Example usage:
printName({ name: "Alice", age: 30 }); // Valid
printName({ name: "Bob" }); // Valid
printName({ age: 25 }); // Error: Property 'name' is missing
printName("Alice"); // Error: Argument must be an object
Explanation
- Uses a generic type
<T>
– The function accepts any object type - The
extends { name: string }
– Ensures that only objects with aname
property are accepted. - Works with extended object types – additional properties are allowed.
Create a mapped type to make all properties of an object optional and demonstrate its use in a function.
// Utility type to make all properties optional
type MakeOptional<T> = { [K in keyof T]?: T[K] };
// Example interface
interface User {
name: string;
age: number;
email: string;
}
// Function demonstrating the usage
function updateUser(user: MakeOptional<User>) {
console.log("Updated user:", user);
}
// Example usage:
updateUser({ name: "Alice" }); // Valid
updateUser({ age: 25, email: "a@b.com" }); // Valid
updateUser({}); // Valid (all properties are optional)
Explanation
[K in key of T]?
– iterates over eachK
(which are extracted fromkeyof T
) and adds?
to make it optional.: T[K]
– preserves the original type ofT
properties.- Applied in the function signature by mapping
User
asT
–MakeOptional<User>
Create two utility types. One extracts only required properties from an interface, the second extracts all optional props
// Utility type to extract only required properties
type RequiredProperties<T> = {
[K in keyof T as T[K] extends Required<Pick<T, K>>[K] ? K : never]: T[K];
};
// Utility type to extract only optional properties
type OptionalProperties<T> = {
[K in keyof T as undefined extends T[K] ? K : never]: T[K];
};
// Example interface
interface User {
name: string; // Required
age?: number; // Optional
email: string; // Required
phone?: string; // Optional
}
// Example usage
const user1: RequiredProperties<User> = {
name: "Alice",
email: "alice@example.com",
// age and phone are omitted because they are optional
};
const user2: OptionalProperties<User> = {
age: 25,
phone: "555-1234",
// name and email cannot be used as they were required
};
Explanation
keyof T
– Extracts all keys from the original type.as T[K] extends Required<Pick<T, K>>[K] ? K : never
- Checks if a property is required by comparing it with
Required<Pick<T, K>>[K]
. - If it’s required, the key is included; otherwise, it’s omitted.
- Checks if a property is required by comparing it with
[K in keyof T as undefined extends T[K] ? K : never]
- Checks if
undefined
is assignable to the property typeT[K]
- This is the TypeScript way to detect optional properties.
- Checks if
Write a recursive type to represent a deeply nested object structure with homogeneous leaf values (T).
// Recursive type to represent a deeply nested object
type NestedObject<T> = {
[key: string]: T | T[] | NestedObject<T> | NestedObject<T>[];
};
// Example usage
const data: NestedObject<string> = {
user: {
name: "Alice",
address: {
city: "New York",
zip: "10001",
},
},
config: {
themes: [ "dark", "light", "auto" ],
},
};
Explanation
NestedObject<T>
defines an object where:
- Each key is a
string
. - The value can be either of type
T
or an array ofT
or anotherNestedObject<T>
(creating recursion) or even an array ofNestedObject<T>
.
Implement a discriminated union type for a function that handles different shapes (Circle, Rectangle, Square).
// Define shape types with a discriminant property "type"
interface Circle {
type: "circle";
radius: number;
}
interface Rectangle {
type: "rectangle";
width: number;
height: number;
}
interface Square {
type: "square";
side: number;
}
// Discriminated union of all shapes
type Shape = Circle | Rectangle | Square;
// Function to calculate area based on shape type
function getArea(shape: Shape): number {
switch (shape.type) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "square":
return shape.side ** 2;
default:
throw new Error("Invalid shape");
}
}
// Example usage:
console.log(getArea({ type: "circle", radius: 5 }));
console.log(getArea({ type: "rectangle", width: 4, height: 6 }));
console.log(getArea({ type: "square", side: 3 }));
Explanation
- Defines shape interfaces (
Circle
,Rectangle
,Square
)- Each shape has a unique
type
property (the discriminant). - Other properties define specific shape dimensions.
- Each shape has a unique
- Uses a Discriminated Union (
Shape
)- Combines all shape types into a single type.
- Implements
getArea()
Function- Ensures only valid properties are accessed.
- If an unknown shape is passed, TypeScript will throw an error.
Write a properly typed function that extracts keys from an object.
// Generic function to get all keys of an object
function getObjectKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
// Example usage:
const user = {
name: "Alice",
age: 30,
email: "alice@example.com",
};
const keys = getObjectKeys(user);
console.log(keys); // Output: ["name", "age", "email"]
Explanation
- Type Assertion (
as (keyof T)[]
) is required becauseObject.keys()
returnsstring[]
- We could omit the function’s return type declaration as it would get inferred from the above-mentioned type assertion. It’s added for clarity.
Implement a generic function that enforces type constraints on an object’s properties.
// Generic function that enforces constraints on object properties
function enforceConstraints<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}
// Example interface
interface User {
name: string;
age: number;
isAdmin: boolean;
}
// Example usage
const user: User = { name: "Alice", age: 30, isAdmin: false };
const updatedUser = enforceConstraints(user, "age", 35); // Valid
const invalidUser = enforceConstraints(user, "age", "thirty"); // Error: Type 'string' is not assignable to type 'number'
console.log(updatedUser); // Output: { name: "Alice", age: 35, isAdmin: false }
Explanation
- Generic Parameters
<T, K extends keyof T>
T
is the object type.K extends keyof T
ensureskey
is a valid property ofT
.value: T[K]
ensuresvalue
is of the correct type for the key (K
).
- Function Behavior
- Takes an object, a property key, and a new value.
- Ensures the property key exists in
T
and the assigned value matches its type.
Define an abstract class Shape with area() as an abstract method and extend it for Circle and Rectangle.
// Abstract class with an abstract method
abstract class Shape {
abstract area(): number; // Must be implemented by subclasses
// Common method for all shapes
displayArea(): void {
console.log(`The area is: ${this.area()}`);
}
}
// Circle class extending Shape
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
// Rectangle class extending Shape
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
area(): number {
return this.width * this.height;
}
}
// Example usage:
const circle = new Circle(5);
circle.displayArea(); // Output: The area is: 78.53981633974483
const rectangle = new Rectangle(4, 6);
rectangle.displayArea(); // Output: The area is: 24
Explanation
- Abstract class
Shape
- Defines an abstract method
area()
that must be implemented in subclasses. - Includes a concrete method
displayArea()
to print the area.
- Defines an abstract method
- Subclass
Circle
implements thearea()
method usingπr²
. - Subclass
Rectangle
implements thearea()
method usingwidth × height
. - Both
Circle
andRectangle
share a common structure while implementing customarea()
logic.
Implement a singleton pattern in TypeScript.
class Singleton {
private static instance: Singleton;
private constructor() {
console.log("Singleton instance created!");
}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
public sayHello(): void {
console.log("Hello from Singleton!");
}
}
// Example usage:
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
instance1.sayHello(); // Output: "Hello from Singleton!"
console.log(instance1 === instance2); // Output: true (same instance)
Explanation
- Private constructor prevents direct instantiation using
new Singleton()
. - Static method
getInstance()
- Ensures only one instance of the class exists.
- If
instance
is not created, it initializes it. - If already created, it returns the existing one.
- Ensures a single shared instance
instance1 === instance2
→ Both variables refer to the same object.
Create a factory function to return different user roles (Admin, Editor, Viewer).
// Define an enum-like object
const Roles = {
Admin: "admin",
Editor: "editor",
Viewer: "viewer",
} as const;
// Derive Role type from the object
type Role = typeof Roles[keyof typeof Roles];
interface User {
name: string;
role: Role;
permissions: string[];
}
// Factory function to create user roles
function createUser(name: string, role: Role): User {
const rolePermissions: Record<Role, string[]> = {
[Roles.Admin]: ["create", "read", "update", "delete"],
[Roles.Editor]: ["read", "update"],
[Roles.Viewer]: ["read"],
};
return {
name,
role,
permissions: rolePermissions[role],
};
}
// Example usage:
const admin = createUser("Alice", Roles.Admin);
const editor = createUser("Bob", Roles.Editor);
const viewer = createUser("Charlie", Roles.Viewer);
console.log(admin);
// { name: 'Alice', role: 'admin', permissions: [ 'create', 'read', 'update', 'delete' ] }
console.log(editor);
// { name: 'Bob', role: 'editor', permissions: [ 'read', 'update' ] }
console.log(viewer);
// { name: 'Charlie', role: 'viewer', permissions: [ 'read' ] }
Explanation
Roles
is an immutable (as const
) enum-like objectRole
type is derived from the values ofRoles
, so it’s always synced- Factory function
createUser()
- Accepts
name
andRole
. - Uses a permissions lookup table (
rolePermissions
) to assign correct permissions.
- Accepts
- The
rolePermissions
object usesRole
as the key type in theRecord
utility type. This ensures that the keys in this object must be one of the definedRole
enum values - Use of the
Roles
object:- provides more descriptive names for the roles, making the code easier to understand and maintain.
- provides code editor’s autocompletion suggestions for the valid role values, further reducing the chance of errors.
Implement a mixin to add logging behavior to a class.
// Mixin function to add logging behavior
type Constructor<T = {}> = new (...args: any[]) => T;
function Logger<T extends Constructor>(Base: T) {
return class extends Base {
log(message: string) {
console.log(`[LOG]: ${message}`);
}
};
}
// Base class
class User {
constructor(public name: string) {}
getName(): string {
return this.name;
}
}
// Applying mixin
const LoggingUser = Logger(User);
const user = new LoggingUser("Alice");
user.log("User created!"); // Output: [LOG]: User created!
console.log(user.getName()); // Output: Alice
Explanation
- Mixin function (
Logger
)- Takes a base class (
Base
). - Returns a new class that extends
Base
and adds alog()
method.
- Takes a base class (
Constructor<T>
type ensuresLogger
works with any class that has a constructor.- Applying the Mixin
const LoggingUser = Logger(User);
creates a new extended class.user.log("User created!")
→ use of the “injected” method.
Write a typed function that uses async/await to fetch data from an API and handles errors properly.
async function fetchData<T>(url: string): Promise<T | null> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
}
return response.json() as T;
} catch (error) {
console.error("Fetch error:", error);
return null; // Return null on failure
}
}
// Example usage:
interface User {
id: number;
name: string;
email: string;
}
(async () => {
const userData = await fetchData<User>("<https://jsonplaceholder.typicode.com/users/1>");
if (userData) {
console.log("User Data:", userData);
} else {
console.log("Failed to fetch user data.");
}
})();
Explanation
- Generic function
fetchData<T>
- Allows fetching any API response type.
- The return type is
Promise<T | null>
to handle errors gracefully.
- Error handling (
try/catch
)- If the fetch request fails, an error is logged, and
null
is returned. response.ok
check ensures HTTP errors are caught.
- If the fetch request fails, an error is logged, and
- Graceful failure handling
- If the API request fails, the caller receives
null
, avoiding crashes.
- If the API request fails, the caller receives
Implement a typed memoization function to optimize expensive calculations.
function memoize<T extends (...args: any[]) => any>(func: T): T {
const cache = new Map<string, ReturnType<T>>();
return function (...args: Parameters<T>): ReturnType<T> {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key) as ReturnType<T>;
}
const result = func(...args);
cache.set(key, result);
return result;
} as T;
}
// Example: Expensive Fibonacci function
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Memoized version
const fastFibonacci = memoize(fibonacci);
// Example usage:
console.log(fastFibonacci(40)); // Computed result
console.log(fastFibonacci(40)); // Returned from cache
Explanation
- Use of
T extends (...args: any[]) => any
serves the following:(...args: any[]) => any
: this part describes a generic function type.(...args: any[])
indicates that the passed function can accept any number of arguments of any type (any
).T extends ...
means that the generic type parameterT
is constrained to be a function that matches the(...args: any[]) => any
structure. In simpler terms,T
must be a function. This ensures that thememoize
function can only be used with function arguments.
- The return type of
memoize
is declared asT
.- This is the key to type preservation. The
memoize
function returns a new function, but this new function is typed exactly the same as the original function that was passed in.
- This is the key to type preservation. The
- The
memoize
function:- stores computed results in a
Map
(cache). - converts function arguments into a JSON string (
key
) for caching.
- stores computed results in a
- Example with fibonacci calculation
- Recursive Fibonacci (
fibonacci
) is expensive (O(2ⁿ)
complexity). memoize(fibonacci)
speeds up repeated calls.
- Recursive Fibonacci (
- Caching Mechanism
- If the function is called with the same arguments, it returns the cached result instead of recomputing.
Write a type-safe event emitter in TypeScript.
// Define a type-safe event map
type UserData = { userId: number };
type EventMap = {
message: string;
login: UserData;
logout: UserData;
};
// Type-Safe Event Emitter
class EventEmitter<T extends Record<string, any>> {
private events: { [K in keyof T]?: ((data: T[K]) => void)[] } = {};
// Subscribe to an event
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event]?.push(listener);
}
// Emit an event
emit<K extends keyof T>(event: K, data: T[K]): void {
this.events[event]?.forEach(listener => listener(data));
}
// Remove a specific listener
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
if (this.events[event]) {
this.events[event] = this.events[event]?.filter(l => l !== listener);
}
}
}
// Example usage
const emitter = new EventEmitter<EventMap>();
// Subscribe to events
emitter.on("message", (msg) => console.log("Message received:", msg));
emitter.on("login", (data) => console.log(`User ${data.userId} logged in`));
// Emit events
emitter.emit("message", "Hello, TypeScript!"); // Message received: Hello, TypeScript!
emitter.emit("login", { userId: 42 }); // User 42 logged in
// Removing a listener
const logoutHandler = (data: UserData) => console.log`"User ${data.userId} logged out`);
emitter.on("logout", logoutHandler);
emitter.emit("logout", { userId: 42 }); // Output: User 42 logged out
emitter.off("logout", logoutHandler);
emitter.emit("logout", { userId: 42 }); // No output (listener removed)
Explanation
EventMap
interface- Defines event names and their corresponding payload types.
- Ensures strict type-checking for emitted and listened events.
EventEmitter<T>
class- Generic (
T extends Record<string, any>
) accepts a strongly typed event map. on<K extends keyof T>(event: K, listener: (data: T[K]) => void)
- Subscribes a listener to an event.
emit<K extends keyof T>(event: K, data: T[K])
- Emits an event with the correct data type.
off<K extends keyof T>(event: K, listener: (data: T[K]) => void)
- Removes a specific event listener.
- Generic (
- Example usage
- Listens to
message
andlogin
events. - Ensures emitted data matches expected types (
emit("login", { userId: 42 })
). - The
logoutHandler
function requires us to define the type ofdata
argument, as TypeScript does not link the function declaration with its usage in the next line. Without that TS would complain that “Parameter ‘data’ implicitly has an ‘any’ type”
- Listens to
Implement a linked list with insert, delete, and search functions.
// Define a Node structure
class ListNode<T> {
value: T;
next: ListNode<T> | null = null;
constructor(value: T) {
this.value = value;
}
}
// Define the Linked List class
class LinkedList<T> {
private head: ListNode<T> | null = null;
// Insert a value at the end
insert(value: T): void {
const newNode = new ListNode(value);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
// Delete a value from the list
delete(value: T): void {
if (!this.head) return;
// If head needs to be deleted
if (this.head.value === value) {
this.head = this.head.next;
return;
}
let current = this.head;
while (current.next && current.next.value !== value) {
current = current.next;
}
if (current.next) {
current.next = current.next.next;
}
}
// Search for a value in the list
search(value: T): boolean {
let current = this.head;
while (current) {
if (current.value === value) return true;
current = current.next;
}
return false;
}
// Print the linked list
print(): void {
let current = this.head;
const values: T[] = [];
while (current) {
values.push(current.value);
current = current.next;
}
console.log(values.join(" -> "));
}
}
// Example usage:
const list = new LinkedList<number>();
list.insert(10);
list.insert(20);
list.insert(30);
list.print(); // Output: 10 -> 20 -> 30
console.log(list.search(20)); // Output: true
console.log(list.search(40)); // Output: false
list.delete(20);
list.print(); // Output: 10 -> 30
Explanation
- Node class (
ListNode<T>
) storesvalue
andnext
reference. - Linked List Class (
LinkedList<T>
)insert(value: T)
: Adds a node at the end.delete(value: T)
: Removes a node by value.search(value: T)
: Checks if a value exists.print()
: Displays the list.
Typescript Developer hiring resources
Our clients
Popular Typescript Development questions
Will AI replace TypeScript developers?
AI will not fully replace TypeScript developers, but it may help in automating some of the tasks: code generation, error detection, debugging, and more. AI-infused tools speed up development by suggesting code, finding bugs, and optimizing performance, hence making a TypeScript developer more productive. Still, solving complex problems, planning architectures, and developing custom features are those creative areas that involve human understanding of context, something that AI input is not yet capable of providing. More likely than replacing the developers, AI is a tool that enhances the productivity of the developers and supports them in performing sophisticated high-level tasks.
What coding language does TypeScript use?
TypeScript actually is a superset to JavaScript; it takes the core language of JavaScript and extends it by adding static typing along with other advanced features. The syntax used in writing TypeScript resembles that of JavaScript, but it adds extra type annotations and interfaces that make sure bugs are prevented and code is better organized. Any TypeScript file, when compiled, gets converted to pure JavaScript, hence it can work in any environment that supports JavaScript-for example, web browsers, Node.js. TypeScript therefore takes the best of JavaScript’s flexibility and strengthens code quality and reliability.
Is TypeScript going to replace JavaScript?
TypeScript is unlikely to completely replace JavaScript, but for the time being, it remains a favorite choice for many developers to produce code that is more structured and resistant to bugs. TypeScript is an extension of JavaScript that adds static typing, among other things, to catch errors early to make the code more maintainable-especially for large-scale applications. However, JavaScript remains the language of the web and has universal support in browsers, not to mention its enormous ecosystem. TypeScript ultimately compiles down to JavaScript, so it does not replace JavaScript but complements it. Many developers like working in TypeScript on complex projects because of the added benefits, but JavaScript will still be used everywhere for a long time, especially on more straightforward projects and in environments that need flexibility.
What is TypeScript used for?
TypeScript is an over-set to JavaScript adding static typing to the language, hence enhancing the reliability and readability of codes. It finds broad applications in the development of large-scale applications on both the frontend and backend by offering such features as type annotations and interfaces, which provide better tooling for capturing errors much earlier in the development flow. Particularly, TypeScript shines with frameworks like Angular, React, and Node.js, where it enables developers to create scalable and maintainable codebases with first-class tooling support. Structuring code and granting type safety, TypeScript makes it easier to handle complex projects and collaborate within larger teams.
What does a TypeScript developer do?
A TypeScript developer develops and maintains applications by utilizing Typescript, which is a superset of JavaScript that is typed. They use type annotations to decrease errors, hence enhancing the maintainability, scalability, and cleanliness of their code. They usually work with frameworks like Angular, React, or Node.js for developing both front-end and back-end applications and ensure that the application logic is strong and less error-prone at runtime. Also, in enforcing standard code, catching issues early by making use of TypeScript tooling, and optimizing performance and scalability for applications, a TypeScript developer works hand in hand with other developers in giant projects.
Interview Questions by role
Interview Questions by skill
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions