DEV Community

Cover image for Typescript : Generic Data Fetch
ZeeshanAli-0704
ZeeshanAli-0704

Posted on

Typescript : Generic Data Fetch

Question: Implement a Type-Safe Generic Data Fetcher

You are tasked with creating a type-safe generic function in TypeScript that fetches data from an API and handles different response types. The function should:

  1. Accept a URL and an optional configuration object for the fetch request.
  2. Use generics to define the expected response data type.
  3. Handle success and error cases with proper TypeScript types.
  4. Return a Promise that resolves to an object containing either the fetched data or an error message.

Requirements:

  • Define an interface for the response structure.
  • Use generics to make the function reusable for different data types.
  • Handle HTTP errors (e.g., non-200 status codes) with a custom error type.
  • Provide a usage example with two different data types (e.g., User and Product).

Bonus:

  • Add type-safe handling for query parameters in the URL.
  • Explain how your implementation ensures type safety.

Example API Endpoints:

  • https://api.example.com/users (returns an array of users)
  • https://api.example.com/products (returns an array of products)

Sample Data Structures:

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

interface Product {
  id: number;
  name: string;
  price: number;
}
Enter fullscreen mode Exit fullscreen mode

Provide the complete TypeScript code, including types/interfaces, the fetch function, and example usage. Then, explain how your code ensures type safety and handles errors.


Expected Answer Outline:

The candidate should provide:

  1. Interfaces/Types:

    • Define a generic response interface (e.g., ApiResponse<T>) to handle success and error cases.
    • Define a custom error type (e.g., ApiError) for HTTP or network errors.
    • Define an interface for the fetch configuration, including query parameters.
  2. Generic Fetch Function:

    • Create a function like fetchData<T>(url: string, config?: FetchConfig): Promise<ApiResponse<T>>.
    • Use TypeScript generics to allow the function to work with any data type (e.g., User[] or Product[]).
    • Implement error handling for network issues and non-200 status codes.
    • Construct the URL with query parameters if provided.
  3. Example Usage:

    • Show how to call fetchData<User[]> for the users endpoint and fetchData<Product[]> for the products endpoint.
    • Demonstrate handling of success and error cases.
  4. Explanation:

    • Describe how generics ensure the response data matches the expected type.
    • Explain how the response interface provides type safety for success and error states.
    • Discuss how query parameters are type-safely appended to the URL.
    • Highlight error handling for robustness.

Sample Solution:

// Define custom error type
interface ApiError {
  message: string;
  status?: number;
}

// Define response structure
interface ApiResponse<T> {
  data?: T;
  error?: ApiError;
}

// Define fetch configuration with query parameters
interface FetchConfig {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  queryParams?: Record<string, string | number>;
}

// Generic fetch function
async function fetchData<T>(url: string, config: FetchConfig = {}): Promise<ApiResponse<T>> {
  try {
    // Construct URL with query parameters
    let finalUrl = url;
    if (config.queryParams) {
      const params = new URLSearchParams();
      for (const [key, value] of Object.entries(config.queryParams)) {
        params.append(key, value.toString());
      }
      finalUrl = `${url}?${params.toString()}`;
    }

    // Make fetch request
    const response = await fetch(finalUrl, {
      method: config.method || 'GET',
      headers: config.headers,
    });

    // Check for HTTP errors
    if (!response.ok) {
      return {
        error: {
          message: `HTTP error: ${response.statusText}`,
          status: response.status,
        },
      };
    }

    // Parse and return data
    const data: T = await response.json();
    return { data };
  } catch (error) {
    // Handle network or other errors
    return {
      error: {
        message: error instanceof Error ? error.message : 'Unknown error occurred',
      },
    };
  }
}

// Sample data interfaces
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

// Example usage
async function main() {
  // Fetch users with query parameters
  const userResponse = await fetchData<User[]>(
    'https://api.example.com/users',
    {
      queryParams: { limit: 10, page: 1 },
      headers: { Authorization: 'Bearer token123' },
    }
  );

  if (userResponse.data) {
    console.log('Users:', userResponse.data);
  } else {
    console.error('User fetch error:', userResponse.error);
  }

  // Fetch products
  const productResponse = await fetchData<Product[]>(
    'https://api.example.com/products',
    {
      queryParams: { category: 'electronics' },
    }
  );

  if (productResponse.data) {
    console.log('Products:', productResponse.data);
  } else {
    console.error('Product fetch error:', productResponse.error);
  }
}

main();
Enter fullscreen mode Exit fullscreen mode

Explanation of Type Safety and Error Handling:

  1. Generics:

    • The T generic type ensures the data property in ApiResponse<T> matches the expected type (e.g., User[] or Product[]). This prevents type mismatches at compile time.
    • For example, calling fetchData<User[]> ensures the data property is typed as User[], and TypeScript will flag any incorrect usage.
  2. Response Structure:

    • The ApiResponse<T> interface uses a union-like structure (data?: T; error?: ApiError) to ensure the response is either successful (data) or failed (error). This forces consumers to handle both cases explicitly.
  3. Query Parameters:

    • The FetchConfig interface allows type-safe query parameters via Record<string, string | number>. The URLSearchParams API ensures parameters are correctly formatted in the URL.
  4. Error Handling:

    • HTTP errors (non-200 status codes) return an ApiError with the status code and message.
    • Network or parsing errors are caught in the try-catch block and return an ApiError with a descriptive message.
    • The ApiError interface ensures errors are structured and type-safe.
  5. Type Safety Benefits:

    • TypeScript enforces that the consumer of fetchData knows the expected data type upfront, reducing runtime errors.
    • The optional data and error properties in ApiResponse ensure the consumer checks for errors before accessing data, preventing null/undefined errors.
    • The FetchConfig interface ensures only valid configuration properties are passed, and query parameters are safely serialized.

Bonus Considerations:

  • The candidate could extend the function to support request bodies for POST/PUT requests by adding a body field to FetchConfig with proper typing.
  • They could add type guards or utility functions to simplify response handling (e.g., isSuccessResponse<T>(response: ApiResponse<T>): response is { data: T }).

This question tests:

  • TypeScript fundamentals (interfaces, generics, union types).
  • Practical API integration with fetch.
  • Error handling and type safety.
  • Ability to explain design decisions.

It’s suitable for intermediate to senior developers and can be scaled down (e.g., remove query params) or up (e.g., add request body handling) based on the candidate’s experience level.

More Details

Check out the full code of this article on All About Typescript.

Get all articles related to system design:

Hashtag: #SystemDesignWithZeeshanAli

GitHub Repository: SystemDesignWithZeeshanAli

Top comments (0)