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
13 Mins Tutorial By Kyle (Inspiration for this blog)