TypeScript Senior Engineer Interview Questions

The most surprising thing about TypeScript is that its primary benefit isn’t catching more bugs, but rather enabling a fundamentally different kind of software design.

Let’s see how a senior engineer might tackle a common problem: building a robust, type-safe API client. Imagine we’re interacting with a hypothetical "User Service" that has endpoints for fetching users by ID, listing all users, and creating a new user.

Here’s a simplified version of the service’s API contract, expressed in TypeScript interfaces:

interface User {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
}

interface UserListResponse {
  users: User[];
  totalCount: number;
}

interface CreateUserResponse {
  id: string;
  message: string;
}

Now, a senior engineer would start building a client that respects these types. Notice how fetch itself is not directly typed to return specific data structures. The real work is in transforming the raw Response into our expected types.

async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json() as Promise<T>; // Type assertion here is crucial
}

class UserService {
  private baseUrl: string;

  constructor(baseUrl: string = 'http://localhost:3000/api/users') {
    this.baseUrl = baseUrl;
  }

  async getUserById(id: string): Promise<User> {
    return fetchJson<User>(`${this.baseUrl}/${id}`);
  }

  async listUsers(): Promise<UserListResponse> {
    return fetchJson<UserListResponse>(this.baseUrl);
  }

  async createUser(userData: Omit<User, 'id'>): Promise<CreateUserResponse> {
    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json() as Promise<CreateUserResponse>;
  }
}

The fetchJson helper is key. It takes a generic type T and returns a Promise<T>. The as Promise<T> is a type assertion. This is where the developer tells TypeScript, "I guarantee that the JSON parsed from this response will conform to the shape of T." This is a powerful signal, but also a potential point of failure if the backend contract changes and this assertion becomes incorrect.

A senior engineer anticipates this. They understand that the runtime reality of network requests might not always match the compile-time contract. So, they build in runtime validation.

// Runtime validation for the User type
function isUser(obj: any): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof obj.id === 'string' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string' &&
    typeof obj.isActive === 'boolean'
  );
}

// Modified fetchJson to include runtime validation
async function fetchJsonWithValidation<T>(
  url: string,
  options: RequestInit | undefined,
  validator: (data: any) => data is T // A validator function is passed in
): Promise<T> {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  const data = await response.json();
  if (validator(data)) {
    return data;
  } else {
    throw new Error('Received data does not conform to expected type.');
  }
}

class UserServiceValidated {
  private baseUrl: string;

  constructor(baseUrl: string = 'http://localhost:3000/api/users') {
    this.baseUrl = baseUrl;
  }

  async getUserById(id: string): Promise<User> {
    return fetchJsonWithValidation<User>(`${this.baseUrl}/${id}`, undefined, isUser);
  }

  // ... other methods would also use fetchJsonWithValidation with appropriate validators
}

This fetchJsonWithValidation function is a significant step up. It doesn’t just assume the JSON is correct; it checks it at runtime using the provided validator function. The validator function is a type predicate (data is T), which tells TypeScript that if the function returns true, the data variable is indeed of type T. This shifts the burden from a potentially unsafe type assertion to an explicit, testable runtime check, while still giving us the compile-time safety of TypeScript.

The mental model here is that TypeScript is a powerful static analysis tool that helps you design your system with explicit contracts. However, for external interfaces like network APIs, these contracts are only enforced at compile time. To bridge the gap between compile-time guarantees and runtime realities, you need to incorporate runtime validation. This involves defining clear, executable checks for your data structures, often alongside your type definitions.

The real power comes when you combine these typed API clients with other TypeScript features like discriminated unions for handling different API response states (e.g., Loading, Success, Error), or generics for creating reusable data fetching or manipulation logic. The ability to define precise shapes for your data and then build functions that operate on those shapes without losing type information is the core of building maintainable, large-scale applications.

A common pitfall is misunderstanding how any can creep into your codebase, especially when dealing with third-party libraries or, as we’ve seen, network responses. While type assertions (as T) can be convenient, they bypass TypeScript’s checks. When used excessively, especially on data from untrusted sources, they can lead to runtime errors that TypeScript would have caught if the assertions were replaced with proper validation.

Ultimately, the next step in mastering TypeScript for senior roles involves understanding how to build robust, fault-tolerant systems that leverage its type system not just for bug prevention, but for architectural clarity and maintainability, especially when integrating with external or unpredictable systems.

Want structured learning?

Take the full Typescript course →