Typescript interview questions and answers for 2025

hero image

TypeScript Interview Questions for Freshers and Intermediate Levels

1.

What is TypeScript, and how does it improve upon JavaScript?

Answer

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:

  1. Static Typing – Detects type errors at compile time, reducing runtime bugs.
  2. Better Code Readability & Maintainability – Enforces strict type rules, making the code easier to understand.
  3. Enhanced IDE Support – Provides autocompletion, refactoring tools, and better error detection in modern editors like VS Code.
  4. Interfaces & Generics – Helps create reusable and scalable code.
  5. 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.

2.

What is type inference in TypeScript?

Answer

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

  1. 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
    
  2. 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
    }
    
  3. 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.

3.

What are interfaces in TypeScript, and how do they differ from type aliases?

Answer

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;
}
4.

How do you define and use optional properties in TypeScript?

Answer

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.

5.

What is the difference between any and unknown in TypeScript?

Answer

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.
6.

How do readonly properties work in TypeScript?

Answer

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.
7.

What is a union type, and how does it work in TypeScript?

Answer

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).
8.

How does TypeScript handle function return types?

Answer

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.
9.

What are generics in TypeScript, and why are they useful?

Answer

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.
10.

How does TypeScript support tuple types, and when would you use them?

Answer

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.
11.

How does method overloading work in TypeScript?

Answer

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 and number inputs).
12.

What is a discriminated union, and how does it improve type safety?

Answer

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).
13.

How do mapped types work in TypeScript?

Answer

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.
14.

What is never in TypeScript, and when should it be used?

Answer

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.
15.

How do keyof and typeof help with type operations in TypeScript?

Answer

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.
16.

What is type assertion, and when should you use it?

Answer

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 or any).
  • 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.
17.

What is the difference between extends and implements in TypeScript?

Answer

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.
18.

What is TypeScript’s strict mode, and why is it recommended?

Answer

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 – Prevents null or undefined from being assigned to non-nullable types.
  • strictPropertyInitialization – Ensures all class properties are initialized before use.
  • noImplicitAny – Requires explicit type annotations instead of allowing any 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.

19.

What are declaration files (.d.ts), and why are they useful?

Answer

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.

20.

How do you ensure type safety when working with external APIs?

Answer

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");
      }
    }
    
21.

How do you use as const in TypeScript, and why is it helpful?

Answer

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 or number.
  • Useful for constants, enums, and fixed-value arrays.

TypeScript Interview Questions for Experienced Levels

1.

How do template literal types improve type safety in TypeScript?

Answer

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.
2.

How do index signatures work, and when should they be used cautiously?

Answer

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 restrictions
    interface 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
    };
    
3.

How does TypeScript handle type narrowing, and how does it improve type safety?

Answer

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.
4.

What is conditional typing, and how is it useful for function signatures?

Answer

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.
5.

How do distributive conditional types work, and when are they useful?

Answer

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 : neverstring
    • number extends string ? nevernever
    • boolean extends string ? nevernever
  • 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 is string | number
  • TypeScript distributes the conditional:
    • string extends any ? string[] : neverstring[]
    • number extends any ? number[] : nevernumber[]
  • Final result: string[] | number[]

When are distributive conditional types useful?

  • Filtering specific types from unions (Extract<T, U> and Exclude<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.
6.

What is the difference between type assertion and type casting, and when should each be used?

Answer

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.
7.

How does structural typing differ from nominal typing, and how does TypeScript handle it?

Answer

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.

8.

How do you create generic constraints in TypeScript to restrict possible types?

Answer

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.
9.

What is the difference between Partial, Required, and Readonly<T>?

Answer

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
10.

How do you extract function argument types using Parameters<T>?

Answer

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.
11.

What is the difference between Exclude<T, U> and Extract<T, U>?

Answer

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.

12.

How can generic utility types help with API response handling?

Answer

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.
13.

How do you implement recursive types for handling deeply nested structures?

Answer

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.
14.

What is the best way to enforce type safety when working with dynamic objects?

Answer

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.

15.

How do abstract classes work, and how do they compare to interfaces?

Answer

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.
16.

How do private, protected, and public access modifiers affect class inheritance?

Answer

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
17.

What is a TypeScript mixin, and how does it compare to class inheritance?

Answer

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.
18.

How do decorators work in TypeScript, and how can they be used?

Answer

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)
19.

How do you optimize TypeScript compilation speed in large projects?

Answer

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.

20.

How does TypeScript’s type system impact JavaScript bundle size, and how can it be optimized?

Answer

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 of enum
    const enum Status {
      Success,
      Error,
    }
    

    const enum removes extra JavaScript runtime objects

  • Enable tree shaking with ES modulesUse "module": "ESNext" in tsconfig.json. This helps remove unused imports, reducing bundle size.
  • Avoid unnecessary decorator metadata
    {
      "compilerOptions": {
        "emitDecoratorMetadata": false
      }
    }
    
  • Minimize runtime helpers with tslibInstall tslib and use "importHelpers": true to reduce redundant helper functions in each file.
  • Use ES2017 or ESNext 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.
21.

How do you type React hooks like useState and useReducer correctly?

Answer

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
22.

How does TypeScript improve maintainability in large-scale React applications?

Answer

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.
23.

What are the challenges of using TypeScript with third-party JavaScript libraries, and how can they be resolved?

Answer

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 ensure tsconfig.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 or default 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 or unknown temporarily:
     // Less safe
    const lib: any = require("some-library");
    
    // Safer but requires type checks
    const lib: unknown = require("some-library");
    

TypeScript Coding Interview Questions

1.

Define a generic function that takes an array and returns its first element.

Answer
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 type T or undefined (when accessing an empty array’s first element).
  • Ensures Type Safety – The returned value will always match the type of elements in the array.
2.

Create a type-safe function that only accepts objects containing a name: string property.

Answer
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 a name property are accepted.
  • Works with extended object types – additional properties are allowed.
3.

Create a mapped type to make all properties of an object optional and demonstrate its use in a function.

Answer
// 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 each K (which are extracted from keyof T) and adds ? to make it optional.
  • : T[K] – preserves the original type of T properties.
  • Applied in the function signature by mapping User as TMakeOptional<User>
4.

Create two utility types. One extracts only required properties from an interface, the second extracts all optional props

Answer
// 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.
  • [K in keyof T as undefined extends T[K] ? K : never]
    • Checks if undefined is assignable to the property type T[K]
    • This is the TypeScript way to detect optional properties.
5.

Write a recursive type to represent a deeply nested object structure with homogeneous leaf values (T).

Answer
// 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 of T or another NestedObject<T> (creating recursion) or even an array of NestedObject<T>.
6.

Implement a discriminated union type for a function that handles different shapes (Circle, Rectangle, Square).

Answer
// 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.
  • 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.
7.

Write a properly typed function that extracts keys from an object.

Answer
// 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 because Object.keys() returns string[]
  • 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.
8.

Implement a generic function that enforces type constraints on an object’s properties.

Answer
// 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 ensures key is a valid property of T.
    • value: T[K] ensures value 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.
9.

Define an abstract class Shape with area() as an abstract method and extend it for Circle and Rectangle.

Answer
// 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.
  • Subclass Circle implements the area() method using πr².
  • Subclass Rectangle implements the area() method using width × height.
  • Both Circle and Rectangle share a common structure while implementing custom area() logic.
10.

Implement a singleton pattern in TypeScript.

Answer
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 === instance2Both variables refer to the same object.
11.

Create a factory function to return different user roles (Admin, Editor, Viewer).

Answer
// 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 object
  • Role type is derived from the values of Roles, so it’s always synced
  • Factory function createUser()
    • Accepts name and Role.
    • Uses a permissions lookup table (rolePermissions) to assign correct permissions.
  • The rolePermissions object uses Role as the key type in the Record utility type. This ensures that the keys in this object must be one of the defined Role 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.
12.

Implement a mixin to add logging behavior to a class.

Answer
// 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 a log() method.
  • Constructor<T> type ensures Logger 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.
13.

Write a typed function that uses async/await to fetch data from an API and handles errors properly.

Answer
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.
  • Graceful failure handling
    • If the API request fails, the caller receives null, avoiding crashes.
14.

Implement a typed memoization function to optimize expensive calculations.

Answer
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 parameter T is constrained to be a function that matches the (...args: any[]) => any structure. In simpler terms, T must be a function. This ensures that the memoize function can only be used with function arguments.
  • The return type of memoize is declared as T.
    • 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.
  • The memoize function:
    • stores computed results in a Map (cache).
    • converts function arguments into a JSON string (key) for caching.
  • Example with fibonacci calculation
    • Recursive Fibonacci (fibonacci) is expensive (O(2ⁿ) complexity).
    • memoize(fibonacci) speeds up repeated calls.
  • Caching Mechanism
    • If the function is called with the same arguments, it returns the cached result instead of recomputing.
15.

Write a type-safe event emitter in TypeScript.

Answer
// 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.
  • Example usage
    • Listens to message and login events.
    • Ensures emitted data matches expected types (emit("login", { userId: 42 })).
    • The logoutHandler function requires us to define the type of data 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”
16.

Implement a linked list with insert, delete, and search functions.

Answer
// 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>) stores value and next 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
Hire Typescript Developers
Hire fast and on budget—place a request, interview 1-3 curated developers, and get the best one onboarded by next Friday. Full-time or part-time, with optimal overlap.
Hire now
Q&A about hiring Typescript Developers
Want to know more about hiring Typescript Developers? Lemon.io got you covered
Read Q&A
Typescript Developer Job Description Template
Attract top Typescript developers with a clear, compelling job description. Use our expert template to save time and get high-quality applicants fast.
Check the Job Description

Hire remote Typescript developers

Developers who got their wings at:
Testimonials
star star star star star
Gotta drop in here for some Kudos. I’m 2 weeks into working with a super legit dev on a critical project, and he’s meeting every expectation so far 👏
avatar
Francis Harrington
Founder at ProCloud Consulting, US
star star star star star
I recommend Lemon to anyone looking for top-quality engineering talent. We previously worked with TopTal and many others, but Lemon gives us consistently incredible candidates.
avatar
Allie Fleder
Co-Founder & COO at SimplyWise, US
star star star star star
I've worked with some incredible devs in my career, but the experience I am having with my dev through Lemon.io is so 🔥. I feel invincible as a founder. So thankful to you and the team!
avatar
Michele Serro
Founder of Doorsteps.co.uk, UK

Simplify your hiring process with remote Typescript developers

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.

image

Ready-to-interview vetted Typescript developers are waiting for your request