Refactor API Response Types with TypeScript Conditional Types

Refactor API Response Types with TypeScript Conditional Types

Last week I was revisiting an old project and decided to add pagination to a single API endpoint response. Easy enough on the backend, and manually updating things on the frontend wouldn’t have been too difficult, but I like to find solutions that scale: what if I had 30 endpoints in this project, and 10 of them needed to handle pagination now? I wanted to avoid code duplication (don’t want to create extra types that are nearly the same), and I didn’t want to add unnecessary fields (forcing FE devs to figure out whether a response will be paginated or not).

This is the setup that needed refactoring to handle Ingredient pagination:

type ApiResponse<T> = {
  data: T;
  status: "success" | "error";
  error?: string;
};
type Recipe = { id: string; title: string; text: string };
type Ingredient = { id: string; name: string; quantity: number };

type GetRecipeResponse = ApiResponse<Recipe>;
type GetIngredientResponse = ApiResponse<Ingredient>;
        

Enter conditional types. Instead of creating multiple “ApiResponse” variants, I created a PaginatedReponse type that checks whether T includes pagination: true and dynamically adds pagination fields:

type PaginatedResponse<T> = T extends { paginated: true }
  ? ApiResponse<{ results: T["data"]; pagination: { page: number; totalPages: number } }>
  : ApiResponse<T["data"]>;

// type with paginated: false
type GetRecipeResponse = PaginatedResponse<{ data: Recipe; paginated: false }>;

// expands to original unpaginated ApiResponse
type GetRecipeResponse = {
  data: Recipe;
  status: "success" | "error";
  error?: string;
};

// type with paginated: true
type GetIngredientsResponse = PaginatedResponse<{ data: Ingredient[]; paginated: true }>;

// expands to new paginated ApiResponse
type GetIngredientsResponse = {
  data: { results: Ingredient[]; pagination: { page: number; totalPages: number } };
  status: "success" | "error";
  error?: string;
};

// in practice, for multiple endpoints
type GetRecipeResponse = PaginatedResponse<Recipe, false>;
type GetUserResponse = PaginatedResponse<User, false>;
type GetIngredientsResponse = PaginatedResponse<Ingredient, true>;
type GetSavedRecipesResponse = PaginatedResponse<SavedRecipe, true>;
        

Not only does this approach fix my small project quickly, it will also scale for future paginated or unpaginated endpoints.

Any other TypeScript features stand out for saving time and headaches?

要查看或添加评论,请登录

社区洞察

其他会员也浏览了