[object Object]

Top TypeScript Interview Questions

Get ready for your TypeScript interview with real-world questions and code examples covering typing, interfaces, generics, unions, and advanced concepts like infer and utility types.

POSTED ON SEPTEMBER 22, 2025

TypeScript has become the go-to language for building large JavaScript applications. Companies from startups to tech giants use it daily. If you’re preparing for a TypeScript interview, you need to know what interviewers actually ask.

This guide covers the most common TypeScript interview questions based on real interviews and industry research. We’ll walk through each concept with examples.

Basic TypeScript Interview Questions

What makes TypeScript different from JavaScript?

TypeScript adds static typing to JavaScript. The compiler checks your code before it runs and catches type errors during development rather than in production.

// JavaScript - fails at runtime
function calculatePrice(quantity, price) {
  return quantity * price;
}
calculatePrice(5, "20"); // Returns "2020020020" - unexpected string concatenation

// TypeScript - catches the error during compilation
function calculatePrice(quantity: number, price: number): number {
  return quantity * price;
}
calculatePrice(5, "20"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

Every JavaScript file works as TypeScript. You can rename .js to .ts and start adding types gradually. This makes migration straightforward for existing projects.

TypeScript is actually a superset of JavaScript, which means all JavaScript code is valid TypeScript code, but with added power from types and features. As a programming language, TypeScript is widely adopted in modern software development, especially in frameworks that emphasize maintainable and scalable codebases.

TypeScript’s compiler uses a powerful type system that enables features like type inference and type annotations, allowing developers to easily manage the type of a variable in complex applications.

What’s the difference between any and unknown?

This question comes up in almost every TypeScript interview. Both types accept any value, but they work differently.

The any type turns off type checking completely. You can perform any operation on an any value without compiler intervention. This makes any dangerous because errors slip through.

let data: any = "hello world";
data.toUpperCase(); // Works fine
data = 42;
data.toUpperCase(); // Runtime error! No compile-time warning

The unknown type is safer. You must check the type before using the value. This preserves type safety while still accepting any value.

let value: unknown = "hello world";

// This fails
value.toUpperCase(); // Error: Object is of type 'unknown'

// This works - we check the type first
if (typeof value === "string") {
  value.toUpperCase(); // TypeScript knows it's a string here
}

Use unknown when you don’t know what type you’ll get. Only use any when migrating old JavaScript code.

TypeScript supports both implicit and explicit type declarations, giving developers flexibility based on their workflow and project complexity.

What are interfaces and type aliases?

Interfaces and type aliases both define object shapes. Developers debate which to use constantly. Each has strengths that matter in different situations.

Interfaces can only describe object shapes, while type aliases offer more flexibility—they can represent primitives, objects, unions, and tuples. When it comes to declaration merging, interfaces can be extended by declaring them multiple times, but type aliases cannot be re-declared.

If you need to combine properties with type aliases, you’ll need to use an intersection type with the & operator. Both interfaces and type aliases work with class implementation, though there’s a catch: a class can implement an interface or an object type, but it cannot implement a union type.

Finally, the syntax differs when extending—interfaces use the extends keyword, while type aliases use the intersection type operator &.

Interfaces work great for objects. You can extend them by declaring the same interface multiple times. This feature is called declaration merging.

interface User {
  name: string;
}

// Add more properties later
interface User {
  email: string;
}

// Both properties are required now
const user: User = {
  name: "Alice",
  email: "alice@example.com"
};

Type aliases handle more than just objects. You can create union types, tuples, and primitives.

// Union type - can be one of several types
type Status = "pending" | "approved" | "rejected";

// Tuple - fixed length array with specific types
type Coordinates = [number, number];

// Function type
type Calculator = (a: number, b: number) => number;

// Intersection - combine multiple types
type Person = {
  name: string;
};

type Employee = Person & {
  employeeId: number;
};

Use interfaces for object shapes in public APIs. Use type aliases for unions, tuples, and complex type operations.

Interfaces also form the basis of object-oriented programming in TypeScript, defining the structure of class members and improving maintainability of the codebase.

What are type guards?

Type guards narrow down types in conditional blocks. They help TypeScript understand what type a variable has in different parts of your code.

function processInput(input: string | number) {
  if (typeof input === "string") {
    return input.toUpperCase();
  }
  return input.toFixed(2);
}

The typeof operator creates a basic type guard. You can also use instanceof for classes and create custom type guards for complex scenarios.

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

// Custom type guard using 'is' keyword
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim();
  } else {
    pet.fly();
  }
}

Custom type guards give you control over how TypeScript narrows types for your specific data structures. Developers often use type assertions within type guards to clarify how certain objects behave.

Common TypeScript Interview Questions

These questions test intermediate knowledge and come up frequently for mid-level positions.

What’s the never type?

The never type represents values that never happen. Functions that throw errors return never because they never return normally:

function throwError(message: string): never {
  throw new Error(message);
}

The never type also helps catch bugs through exhaustive checking. This pattern forces you to handle all cases in a union type:

type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
  switch (shape) {
    case "circle":
      return Math.PI * 5 * 5;
    case "square":
      return 10 * 10;
    case "triangle":
      return 0.5 * 5 * 8;
    default:
      // If we forgot a case, this line causes a compile error
      const exhaustiveCheck: never = shape;
      return exhaustiveCheck;
  }
}

If you add a new shape to the union but forget to handle it in the switch statement, TypeScript will complain. The code won’t compile until you add the missing case.

Enums are another feature that work well with union types, allowing you to create named constants that improve readability and error checking.

What are .d.ts files?

Declaration files end in .d.ts and contain only type information. No implementation code goes in these files. They describe the shape of JavaScript code that exists elsewhere.

When you install packages like @types/node or @types/react, you’re getting declaration files. TypeScript reads these files to understand what functions and properties are available in those libraries.

// math.d.ts - describes a JavaScript library
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export const PI: number;

The actual JavaScript implementation lives in a separate file. The .d.ts file just tells TypeScript what types to expect.

Writing declaration files for legacy JavaScript libraries is common when migrating projects to TypeScript. This makes it easier when using TypeScript for existing codebases.

What are discriminated unions?

Discriminated unions use a common property to tell different types apart. This property is called the discriminant. It’s usually a string literal type that identifies which variant you’re dealing with.

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

interface Triangle {
  kind: "triangle";
  base: number;
  height: number;
}

type Shape = Circle | Square | Triangle;

The kind property is the discriminant. TypeScript uses it to narrow the type automatically in switch statements:

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

This pattern makes handling different variants type-safe. You can’t accidentally access the wrong properties because TypeScript narrows the type in each branch.

Conditional types often work with function overloading, where different versions of a function have different parameter and return value types.

How do generics work?

Generics let you write code that works with multiple types while staying type-safe. Think of them as type variables that get filled in when you use the function or class.

// Without generics - need separate functions
function getFirstNumber(arr: number[]): number {
  return arr[0];
}

function getFirstString(arr: string[]): string {
  return arr[0];
}

// With generics - one function handles all types
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

const firstNumber = getFirst([1, 2, 3]); // TypeScript infers T as number
const firstString = getFirst(["a", "b", "c"]); // TypeScript infers T as string

The angle brackets <T> introduce a type parameter. When you call getFirst, TypeScript figures out what T should be based on the array you pass in.

You can constrain generics to specific types using the extends keyword:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello"); // Works - strings have length
logLength([1, 2, 3]); // Works - arrays have length
logLength(42); // Error - numbers don't have length

Advanced TypeScript Interview Questions

These questions differentiate senior developers from mid-level ones. Interviewers use them to assess deep TypeScript knowledge.

What are conditional types?

Conditional types choose between two types based on a condition. The syntax looks like a ternary operator but works at the type level. These types separate junior developers from senior ones.

The basic pattern is T extends U ? X : Y. If T can be assigned to U, the type becomes X. Otherwise it becomes Y.

type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

// Practical example
type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

The power comes from combining conditional types with the infer keyword.

Conditional types often work with function overloading, where different versions of a function have different parameter and return value types.

TypeScript Classes and Inheritance

TypeScript supports abstract classes, access modifiers, and inheritance between a base class and a child class. These features add clarity and structure to both frontend and backend development.

abstract class Vehicle {
  abstract move(): void;
}

class Car extends Vehicle {
  move() {
    console.log("Car is moving");
  }
}

This concept helps reinforce encapsulation and better separation of logic between components.

You can use console.log to debug class methods or track data flow within a TypeScript project.

Modular Architecture

Developers often divide applications into modules to organize reusable functionality. Each module might export callback functions, classes, or constants that other files import. This modular structure makes your code more reusable and maintainable.

Compiler Configuration

What compiler options should you know?

The tsconfig.json file controls how TypeScript compiles your code. Several flags matter most for interviews.

{
  "compilerOptions": {
    "strict": true
  }
}

This enables strict mode, which improves maintainability and type safety across the entire codebase.

How does the infer keyword work?

The infer keyword lets TypeScript figure out and extract types from other types. You use it inside conditional types to capture a type that matches a pattern.

Here’s how to extract array element types:

type ElementType<T> = T extends (infer U)[] ? U : T;

type Numbers = number[];
type NumberElement = ElementType<Numbers>; // number

type NotAnArray = string;
type StillString = ElementType<NotAnArray>; // string

Here’s what happens: TypeScript checks if T is an array. If it is, infer U captures the element type and returns it. If not, it returns T unchanged.

You can extract return types from functions:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { name: "Alice", age: 30 };
}

type User = ReturnType<typeof getUser>; // { name: string; age: number }

You can also extract function parameters:

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function createUser(name: string, age: number, email: string) {
  return { name, age, email };
}

type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]

The infer P captures the entire parameter list as a tuple. This becomes useful when building wrapper functions or decorators that need to preserve the original function’s signature.

What are mapped types?

Mapped types create new types by transforming properties of existing types. They work like a type-level for-loop and form the foundation of TypeScript’s utility types.

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

interface User {
  name: string;
  age: number;
}

type ReadonlyUser = Readonly<User>;
// {
//   readonly name: string;
//   readonly age: number;
// }

The [P in keyof T] iterates over each property in T. You can add modifiers like readonly or make properties optional with ?:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type PartialUser = Partial<User>;
// {
//   name?: string;
//   age?: number;
// }

You can also transform property names using template literal types:

type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

How do you implement utility types?

Understanding how built-in utility types work shows deep TypeScript knowledge. Senior-level interviews often ask candidates to recreate these from scratch.

Pick selects specific properties from a type:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

Omit excludes specific properties:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; age: number }

Record creates an object type with specified keys and values:

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

type PageInfo = Record<"home" | "about" | "contact", { title: string; url: string }>;
// {
//   home: { title: string; url: string };
//   about: { title: string; url: string };
//   contact: { title: string; url: string };
// }

TypeScript Interview Questions for React

How do you type React components?

React components need types for props. Function components have become the standard way to write React code.

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: "primary" | "secondary";
}

function Button({ label, onClick, disabled = false, variant = "primary" }: ButtonProps) {
  return (
    <button 
      onClick={onClick} 
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
}

Generic components work with any data type:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage
<List<number> 
  items={[1, 2, 3]} 
  renderItem={(num) => <span>Number: {num}</span>} 
/>

How do you type custom hooks?

Custom hooks follow the same typing rules as regular functions. The return type usually includes state and functions to modify that state.

Properly typed hooks enable type-safe reuse of logic across components.

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null
  });
  
  useEffect(() => {
    let cancelled = false;
    
    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled) {
          setState({ data: null, loading: false, error });
        }
      });
    
    return () => {
      cancelled = true;
    };
  }, [url]);
  
  return state;
}

// Usage with typed data
interface User {
  id: number;
  name: string;
  email: string;
}

function UserList() {
  const { data, loading, error } = useFetch<User[]>("/api/users");
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

How do you type Higher-Order Components (HOCs)?

Higher-Order Components wrap existing components to add functionality. Typing them correctly, especially with generics, tests advanced TypeScript knowledge. This pattern appears frequently in senior-level React interviews.

An HOC is a function that takes a component and returns a new component with augmented properties:

import React, { ComponentType } from 'react';

// Simple HOC that injects a user prop
interface InjectedProps {
  user: {
    id: number;
    name: string;
  };
}

function withUser<P extends InjectedProps>(
  Component: ComponentType<P>
): ComponentType<Omit<P, keyof InjectedProps>> {
  return (props) => {
    const user = { id: 1, name: "Alice" };
    
    return <Component {...(props as P)} user={user} />;
  };
}

// Usage
interface ProfileProps extends InjectedProps {
  title: string;
}

function Profile({ user, title }: ProfileProps) {
  return (
    <div>
      <h1>{title}</h1>
      <p>User: {user.name}</p>
    </div>
  );
}

const EnhancedProfile = withUser(Profile);

// Now you can use it without passing user prop
<EnhancedProfile title="My Profile" />

The type signature must correctly define the intersection of the original component’s props and the new props introduced by the HOC. The Omit utility removes injected props from the wrapped component’s required props.

TypeScript Interview Questions for Node.js

How do you type Express middleware?

Express applications need typed middleware for request handling. You can extend Express types to add custom properties through declaration merging.

import { Request, Response, NextFunction } from 'express';

// Basic middleware
function logger(req: Request, res: Response, next: NextFunction): void {
  console.log(`${req.method} ${req.path}`);
  next();
}

Adding custom properties to the Request object:

declare global {
  namespace Express {
    interface Request {
      user?: {
        id: number;
        email: string;
        role: string;
      };
    }
  }
}

// Now you can use req.user safely
function authenticate(req: Request, res: Response, next: NextFunction): void {
  const token = req.headers.authorization;
  
  if (!token) {
    res.status(401).json({ error: 'No token provided' });
    return;
  }
  
  req.user = {
    id: 1,
    email: 'user@example.com',
    role: 'admin'
  };
  
  next();
}

function getProfile(req: Request, res: Response): void {
  if (!req.user) {
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }
  
  res.json({ user: req.user });
}

This same declaration merging pattern works for extending other global objects. For the browser window object, create a global.d.ts file:

// global.d.ts
interface Window {
  __APP_CONFIG__: {
    apiUrl: string;
    version: string;
  };
}

// For Node.js environment variables
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test';
      DATABASE_URL: string;
      API_KEY: string;
    }
  }
}

TypeScript Interview Questions for Angular

Angular is a TypeScript-first framework, making TypeScript proficiency essential for Angular developers. Angular interviews heavily emphasize RxJS and Observable typing.

What are Observables and how do you type them?

RxJS (Reactive Extensions for JavaScript) is a library for handling asynchronous data streams. Key concepts include: Observables are objects that emit multiple values over time, Observers are the consumers/listeners of the stream, and Operators are methods like mapfilter, and switchMap that modify the data stream.

import { Observable, Observer } from 'rxjs';
import { map, filter } from 'rxjs/operators';

// Creating a typed Observable
const numbers$: Observable<number> = new Observable((observer) => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
});

// Consuming with typed Observer
const observer: Observer<number> = {
  next: (value) => console.log('Received:', value),
  error: (err) => console.error('Error:', err),
  complete: () => console.log('Complete')
};

numbers$.subscribe(observer);

Practical example with HTTP requests:

interface User {
  id: number;
  name: string;
  email: string;
}

function getUsers(): Observable<User[]> {
  return new Observable((observer) => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        observer.next(data);
        observer.complete();
      })
      .catch(err => observer.error(err));
  });
}

// Using operators with types
const activeUserEmails$: Observable<string[]> = getUsers().pipe(
  map(users => users.filter(u => u.email.includes('@'))),
  map(users => users.map(u => u.email))
);

What’s the difference between Observable and Promise?

This question tests understanding of asynchronous patterns.

A Promise resolves with a single value, while an Observable can emit multiple values over time. Promises execute eagerly—they start running immediately when created—but Observables are lazy and only execute when you subscribe to them.

Once a Promise starts, you cannot cancel it, but Observables give you control through the unsubscribe() method, letting you stop the stream whenever needed.

Promises offer limited operators like then and catch, whereas Observables come with a rich operator library including map, filter, switchMap, and many more for transforming data streams.

// Promise - single value, eager execution
const userPromise: Promise<User> = fetch('/api/user/1')
  .then(res => res.json());

userPromise.then(user => console.log(user)); // Executes immediately

// Observable - multiple values, lazy execution
import { interval } from 'rxjs';
import { take } from 'rxjs/operators';

const ticks$: Observable<number> = interval(1000).pipe(take(5));

// Doesn't execute until subscribed
const subscription = ticks$.subscribe(tick => console.log('Tick:', tick));

// Can cancel
setTimeout(() => {
  subscription.unsubscribe(); // Stops emitting
}, 3000);

A Promise resolves with a single value and cannot be cancelled once started. An Observable can emit multiple values over time, is lazy (only executes upon subscription), and can be cancelled using unsubscribe().

Compiler Configuration Questions

What compiler options should you know?

The tsconfig.json file controls how TypeScript compiles your code. Several flags matter most for interviews.

The strict flag enables all strict type-checking options at once:

{
  "compilerOptions": {
    "strict": true
  }
}

This enables strictNullChecksnoImplicitAnystrictFunctionTypes, and other safety checks. Here’s what the major options catch:

// strictNullChecks - null and undefined must be explicit
let name: string;
name = null; // Error!

let optionalName: string | null;
optionalName = null; // OK

// noImplicitAny - must specify types explicitly
function process(data) { // Error: Parameter 'data' implicitly has 'any' type
  return data.length;
}

function processTyped(data: string[]) { // OK
  return data.length;
}

// strictPropertyInitialization - class properties need initialization
class User {
  name: string; // Error: Property 'name' has no initializer
  age: number = 0; // OK - initialized inline
  email: string;
  
  constructor(name: string, email: string) {
    this.name = name; // OK - initialized in constructor
    this.email = email;
  }
}

Enabling strict catches many common mistakes before they reach production. Most professional teams leave it enabled for maximum safety.

Master TypeScript for Your Next Interview

These questions cover what interviewers actually ask. Practice writing the code examples yourself rather than just reading them. Build small projects that use these patterns. The concepts stick better when you apply them to real problems.

Ready to strengthen your TypeScript skills? Mimo’s TypeScript course provides interactive lessons that build from fundamentals to advanced patterns. Each lesson includes hands-on coding exercises with instant feedback, helping you practice the exact concepts that come up in technical interviews.

Henry Ameseder

AUTHOR

Henry Ameseder

Henry is the COO and a co-founder of Mimo. Since joining the team in 2016, he’s been on a mission to make coding accessible to everyone. Passionate about helping aspiring developers, Henry creates valuable content on programming, writes Python scripts, and in his free time, plays guitar.

Learn to code and land your dream job in tech

Start for free