Back to Blog
Lesson 21 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 8, 20263 min read

Generics with Interfaces: Building Flexible TypeScript Models

Master Generics with Interfaces in TypeScript. Learn to create reusable API wrappers and enforce consistent data models across your production-grade apps.

TypeScriptGenericsInterfacesReusabilityData Models
Futuristic abstract artwork showcasing AI concepts with digital text overlays.

Previously in this course, we explored Introduction to Generics: Writing Reusable TypeScript Code to handle flexible function logic. In this lesson, we level up by applying those same concepts to interfaces, allowing us to build data structures that adapt to the specific needs of our growing task API client.

Defining Generic Interfaces

In Defining Object Shapes with Interfaces in TypeScript, we learned how to pin down the exact shape of a Task. However, as our application grows, we often receive data that follows a consistent "envelope" or "wrapper" pattern, where the content inside changes but the structure surrounding it remains the same.

A generic interface allows us to define this wrapper once and inject the specific data type later.

TYPESCRIPT
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

By adding <T> after the interface name, we tell TypeScript: "This interface expects a type parameter." Now, we can use ApiResponse for any data structure without redefining the wrapper.

Creating Reusable API Wrappers

Let’s apply this to our running project. Our task API might return a list of tasks, or a single task, or perhaps a user profile. Instead of writing TaskResponse, UserResponse, and ProjectResponse, we use our generic interface.

TYPESCRIPT
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

// Reusable response type
const taskResponse: ApiResponse<Task[]> = {
  data: [{ id: 1, title: CE9178">'Learn Generics', completed: true }],
  status: 200,
  message: CE9178">'Success'
};

const userResponse: ApiResponse<{ username: string }> = {
  data: { username: CE9178">'dev_user' },
  status: 200,
  message: CE9178">'Success'
};

This approach enforces consistency in our data models. Every response across our entire application is guaranteed to have the same status and message fields, while the data payload remains strictly typed to the specific object we expect.

Practice Exercise

Define a generic interface called PaginatedResponse<T> that includes an items array of type T, a totalCount of type number, and a page of type number. Then, create a variable typed with this interface to represent a page of Task objects.

(Hint: items: T[])

Common Pitfalls

  1. Over-complicating with too many generics: If an interface only ever holds one specific type, a generic might be overkill. Only use them when you find yourself copy-pasting the same interface structure for different types.
  2. Confusing any with <T>: Using any bypasses type checking entirely. By using a generic, you maintain strict type safety throughout your data pipeline.
  3. Missing constraints: Sometimes your generic needs to be "at least" a certain shape. We will cover how to enforce these boundaries in the next lesson.

FAQ

Why use an interface instead of a type alias for generics? Interfaces are generally more performant for the compiler and offer better error messages. They also support declaration merging, which is useful in large-scale applications.

Can I have multiple generic parameters? Yes. You can define interface Map<K, V> { key: K; value: V; }. This is helpful for complex data structures like dictionaries or key-value pairs.

Recap

We’ve moved from static object shapes to flexible, generic interfaces. By using <T>, we created a reusable ApiResponse wrapper that ensures consistent data modeling across our project. This keeps our code DRY and our API responses predictable, providing a solid foundation for the more advanced patterns we'll explore as we continue building our task client.

Up next: Constraining Generics

Similar Posts