Generics in TypeScript: A Complete Guide (Part 2)

Generics in TypeScript: A Complete Guide (Part 2)

Β·

8 min read

In the previous part of this blog, we learned what generics are and how to use them in functions. In this part, we will see how to use generics in types, and how to give default values to generic types.

How to use generics in types

Generics are not only useful for functions, but also for types. For example, suppose you have a type that represents an API response πŸ‘‡

type ApiResponse = {
  data: any;
  isError: boolean;
};

This type has two properties: data, which can be any type, and isError, which is a boolean. You can use this type to create an object that holds the response from an API call, like this:

let response: ApiResponse = {
  data: { name: "ID", age: 15 },
  isError: false,
};

This object has the type ApiResponse, and its data property has the value { name: "ID", age: 15 }. However, this is not very type-safe 😬, because the data property can be anything, and TypeScript will not check its type. For example, you could also assign a number or a string to the data property, and TypeScript will not complain πŸ‘‡

let response: ApiResponse = {
  data: 42,
  isError: false,
};

This is not ideal, because we want the data property to have a specific type, depending on the API that we are calling. For example, if we are calling an API that returns a user, we want the data property to have the type { name: string; age: number }. If we are calling an API that returns a blog, we want the data property to have the type { title: string }. How can we achieve this? πŸ€”

The answer is to use generics! We can make the ApiResponse type a generic type, by adding angle brackets (<>) after the type name, and giving a name to the generic type. For example, we can write it like this πŸ‘‡

type ApiResponse<Data> = { //Data being the name of our generic
  data: Data;
  isError: boolean;
};

Here, we have defined a generic type called Data, which can be any type. We have used this type to specify the type of the data property. This means that the ApiResponse type will have a data property of any type, depending on the generic type that we use.

Now, we can use the generic type for different types of data, For eg:

let userResponse: ApiResponse<{ name: string; age: number }> = {
  data: { name: "ID", age: 15 },
  isError: false,
};

This object has the type ApiResponse<{ name: string; age: number }>, and its data property has the type { name: string; age: number }. TypeScript will check the type of the data property, and make sure that it matches the generic type that we use. For example, if we try to assign a different type to the data property, we will get an error πŸ‘‡

let userResponse: ApiResponse<{ name: string; age: number }> = {
  data: "Hello", // Error: Type 'string' is not assignable to type '{ name: string; age: number; }'.
  isError: false,
};

We can also use the generic type for other types of data, like this:

let blogResponse: ApiResponse<{ title: string }> = {
  data: { title: "How to use Generics in TypeScript" },
  isError: false,
};

We can also make the code more readable, by creating aliases for the specific versions of the ApiResponse type, like below πŸ‘‡

type UserResponse = ApiResponse<{ name: string; age: number }>;
type BlogResponse = ApiResponse<{ title: string }>;

let userResponse: UserResponse = {
  data: { name: "ID", age: 15 },
  isError: false,
};

let blogResponse: BlogResponse = {
  data: { title: "How to use Generics in TypeScript" },
  isError: false,
};

This way, we can reuse the ApiResponse type for different types of data, by using generics.

How to give default values to generic types

Sometimes, we might want to give a default value to a generic type, in case we don’t specify the generic type when we use it. For example, suppose we want our our type to represent a status response πŸ‘‡

type StatusResponse = ApiResponse<{ status: number }>;

We can use this type to create an object holding the status response from an API call

let statusResponse: StatusResponse = {
  data: { status: 200 },
  isError: false,
};

However, this type is very common, and we might want to use it as the default type for the ApiResponse type, in case we don’t specify the generic type. How can we do that?

The answer is to use the equal sign (=) after the generic type name, and give the default value. For example, in our case:

type ApiResponse<Data = { status: number }> = {
  data: Data;
  isError: boolean;
};

Here, we have defined a generic type called Data, which can be any type, but has a default value of { status: number }. This means that the ApiResponse type will have a data property of any type, depending on the generic type that we use, but if we don’t use any generic type, it will have the default value of { status: number }.

Now, we can use the generic type without specifying the generic type, and it will use the default value

let statusResponse: ApiResponse = {
  data: { status: 200 }, //data can only expect object of type {status: number} by default
  isError: false,
};

Here, TypeScript will check the type of the data property, and make sure that it matches the default value. For example, if we try to assign a different type to the data property, we will get an error, like this:

let statusResponse: ApiResponse = {
  data: { message: "OK" }, // Error: Type '{ message: string; }' is not assignable to type '{ status: number; }'.
  isError: false,
};

We can, if we want, use the generic type with a different type, and it will override the default value, like below πŸ‘‡

let userResponse: ApiResponse<{ name: string; age: number }> = {
  data: { name: "ID", age: 15 },
  isError: false,
};

This way, we can give a default value to a generic type, and make it more convenient to use.

How to use generics with constraints

Sometimes, we might want to limit the types that a generic type can accept, by using constraints. For example, suppose we want our type to represent an API response:

type ApiResponse<Data> = {
  data: Data;
  isError: boolean;
};

Here, if we want to make sure that the data property is always an object, not a primitive value like a number or a string? How can we do that?

The answer is to use the extends keyword after the generic type name, and give the constraint type πŸ‘‡

type ApiResponse<Data extends object> = {
  data: Data;
  isError: boolean;
};

Now, we can use the generic type with an object type, and it will work as expected:

let userResponse: ApiResponse<{ name: string; age: number }> = {
  data: { name: "ID", age: 15 },
  isError: false,
};

And, if we try to assign a primitive type to the data property, we will get an error πŸ‘‡

let userResponse: ApiResponse<{ name: string; age: number }> = {
  data: 42, // Error: Type 'number' is not assignable to type '{ name: string; age: number; }'.
  isError: false,
};

We can also use the generic type with another object type, and it will work as expected. For eg:

let blogResponse: ApiResponse<{ title: string }> = { //just make sure the generic is of type object
  data: { title: "How to use Generics in TypeScript" },
  isError: false,
};

How to give default values to constrained generics?

Sometimes, we might want to give a default value to a generic type with constraints, in case we don’t specify the generic type when we use it. For example, with a type being very common, we might want to use it as the default type for the ApiResponse type, in case we don’t specify the generic type. How can we do that?

We can do this by combining the above two operations we learnt above, using (=) after the generic type name, and give the default value. For example:

type ApiResponse<Data extends object = { status: number }> = {
  data: Data;
  isError: boolean;
};

Here, we have defined the ApiResponse type to have a data property of any object type, depending on the generic type that we use, but if we don’t use any generic type, it will have the default value of { status: number }.

Now, we can use the generic type without specifying the generic type, and it will use the default value:

let statusResponse: ApiResponse = {
  data: { status: 200 },
  isError: false,
};

Conclusion

Congratulations, You made it! πŸŽ‰πŸŽ‰πŸŽ‰

In this blog series, we learned how to use generics in TypeScript, and how they can help us write flexible and reusable code. We saw how to use generics in functions and types, and how to use generics with constraints and default values.

Overall, Generics are an important backbone of TypeScript that allow us to write code that can work with different types of data, without losing the type information. I hope you enjoyed this blog, and learned something new.

If you liked this blog, please share it with your friends and follow me on Twitter for more web development tips and tricks.

Thank you for reading! ✌️🌟

More Resources

Β