DEV Community

Cover image for Understanding TypeScript Generics: A Deep Dive with Practical Examples
Aditya Raj
Aditya Raj

Posted on

Understanding TypeScript Generics: A Deep Dive with Practical Examples

TypeScript generics were messing with my head, so I decided to create a simple example to better explain it to others. Now I feel more confident in declaring generics.

I generally use Sveltekit to develop websites and web apps, and TypeScript is inevitable there.

TypeScript has become an essential tool for many developers, offering strong typing and improved code quality. One of its most powerful features is generics. In this post, we'll explore how generics work and why they're so valuable, using a practical example to illustrate their benefits.

The Problem: Type Safety vs. Flexibility

When working with TypeScript, we often face a dilemma: we want our code to be flexible enough to handle different data types, but we also want to maintain strong type safety. This is where generics come in.

Let's look at a common scenario: fetching data from an API.

A Generic Function for Fetching Data

Consider this function that fetches data from a given URL:

async function getDummyData<Type>(url: string): Promise<Type> {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return data;
  } catch (error) {
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's what's happening:

  1. We define a generic type parameter <Type>.
  2. The function takes a url parameter of type string.
  3. It returns a Promise<Type>, indicating that it will eventually resolve to our specified type.
  4. We use async/await to handle the asynchronous nature of the fetch operation.
  5. The function fetches data from the URL, parses it as JSON, and returns it.

The Power of Generics

Without generics, we might be tempted to use any as the return type:

async function getDummyData(url: string): Promise<any> {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

But this approach has significant drawbacks:

  1. It removes type safety.
  2. It eliminates helpful autocompletion in our IDE.
  3. It negates many of the benefits of using TypeScript in the first place.

As the transcript mentions, "Any is going to not provide us any benefit of using TypeScript."

Defining Our Types

To really see the benefits of generics, let's define some types for our data:

interface Post {
  id: number;
  title: string;
  body: string;
  tags: string[];
  reactions?: {
    likes: number;
    dislikes: number;
  };
  views: number;
  userId: number;
}

type PostsArray = Post[];
Enter fullscreen mode Exit fullscreen mode

Now we can use our generic function with these types:

const url = "https://api.example.com/posts";
const response = await getDummyData<PostsArray>(url);
Enter fullscreen mode Exit fullscreen mode

The Benefits of Using Generics

  1. Type Safety: TypeScript now knows exactly what type of data we're working with.
  2. Intellisense Support: Our IDE can provide accurate autocompletion and type information.
  3. Error Prevention: TypeScript will catch type-related errors at compile-time.

For example:

// This will cause a TypeScript error
response.id; // Error: Property 'id' does not exist on type 'PostsArray'

// This works perfectly, with full autocompletion
console.log(response[0].title);

// We get type safety for nested properties too
console.log(response[3].reactions?.likes);
Enter fullscreen mode Exit fullscreen mode

As the transcript points out, "we are getting some type safety over here which we wouldn't have had if we would have just gone with the any type."

Flexibility of Generics

One of the key advantages of generics is that they allow us to define the type at the time of function call. This means we can use the same function for different data structures:

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

// Fetching posts
const posts = await getDummyData<PostsArray>("https://api.example.com/posts");

// Fetching a single user
const user = await getDummyData<User>("https://api.example.com/user/1");
Enter fullscreen mode Exit fullscreen mode

Handling Nested Data Structures

Sometimes, API responses might have a more complex structure. For instance, our posts might be nested under a data property:

interface ApiResponse<T> {
  data: T;
  meta: {
    total: number;
    page: number;
  };
}

const response = await getDummyData<ApiResponse<PostsArray>>(url);
console.log(response.data[0].title); // Correctly typed!
console.log(response.meta.total); // Accessing metadata
Enter fullscreen mode Exit fullscreen mode

This example demonstrates how we can use generics to handle complex, nested data structures while maintaining full type safety.

Conclusion

Generics in TypeScript provide a powerful way to write flexible, reusable code while maintaining strong type safety. By using generics in functions like our getDummyData example, we can create versatile utilities that work with various data types without sacrificing the benefits of TypeScript's type system.

As you continue to work with TypeScript, you'll find that generics become an indispensable tool in your programming toolkit, helping you write more robust and maintainable code. They allow you to "retain the flexibility of passing the type argument later to the function and to also ensure type safety throughout the wrap," as mentioned in the transcript.

I work at company, where we develop custom integrations for QuickBooks and other business solutions. TypeScript helps us out in developing robust integration with minimal runtime error.

What are some benefits that you've gained from TypeScript?

Top comments (0)