How to better understand TS generics? the basics
This is the first blog post in a series of articles. In this series I will try to help you understand all the one letter words and triangle brackets in your codebase.
In many languages such as C#
and Java
, the concept of Generics has existed for a long time.
Generics are tools used to create reusable code; they allow us to create code that can work with multiple types.
What are Generics in typescript?
Generics in typescript derive from the same idea as other languages.
They are a way to create types from other types, and they allow us to parameterize types.
Enough with the big words, let’s understand why Generics are useful tools from a concrete example.
Let's create a function that returns the first element of an array.
The first use case is using a number array:
Some days have passed and we are pretty happy with our new head
function, but now we need to get the first element from a string array.
Ok that’s not a problem, let’s add support for strings.
First, we try to use a union type.
We can feel that something is wrong here.
When using numbers array as the input to head
, we know that there is no way that we can get back a string.
We can try using any
:
But we get no type information for firstNum
, we know it can only be a number
or undefined
but with using any
we lose this information.
Typescript Generics to the rescue!
head
is a reusable function, the only thing that it needs to know is that it is given an array.
And the return type will be the type of element that is in the array, or undefined
.
To rewrite head
types let’s understand what Type parameters are.
Type parameters are placeholders for a specific type.
<T>
The triangle brackets to the right ofliftArray
is where we defined the ‘type parameters’. We can define one or more, separated with a comma<T,E>
t:T
Is where we specify the type oft
, which is our function input, we specify the type parameterT
:T[]
We state the return value is an array of type parameterT
Let’s see when T
gets its specific value:
In this case T
is getting the number
type as we call liftArray<number>.
At this point, we create an instance of the generic type.
Usually, you don’t see the caller of the function specify the type as we did.
Instead, you see liftArray(5)
, because Typescript can infer the type from the function input.
Back to our example of creating head
.
We add a type parameter to head
function to make it generic.
We added T
as a type parameter to head<T>
, then we used this parameter for the input arr: T[]
which indicates that it’s an array of type T
.
And we stated the return type T | undefined
.
From lines 5 to 8 we can see the magic of generics in action.
We can send any type of array to head
and we will get back an element that is the same type as an array element.
Using numArr
of type number[]
typescript infer that T = number
and we get back number
or undefined
.
Works the same for string
using strArr
.
Generic allowed us to create highly reusable and type safe code.
Challenge - build a generic map function:
Build a generic function that receives an array and a mapping function.
- Make the code compile.
map
return value type should be the same as the array element.- Don’t use
Array.prototype.map()
.
Click on the link below to start hacking:
Check your solution here:
Generics type parameters are useful but as in many other fields in programming, adding constraints can make something better.
Generic Constraints
Generics constraints are a way to restrict our type parameters and reduce the possible types that they can be.
To explain why we need to restrict our types I’ll use an example:
We are running an e-commerce site for a pet store.
We want to create a function that returns the total amount to pay at the checkout.
We have many types of items in our store and each one of them is represented using an object.
For example, dog food is represented by:
type DogFood = {
mainIngredient: 'salmon' | 'beef' | 'chicken';
size: 'big' | 'small' | 'medium';
price: number;
};
Each one of these objects has many different properties but they all have price:number
property.
First, let’s try to use generics with no constraints
Unfortunately, this code will not compile,
The compiler error:
Property 'price' does not exist on type 'T'
Typescript does not have any way to know that cartItem
has the property price
on it.
With no constraints on T
, it basically functions as any
inside calcCartTotal
.
It’s important to notice that in the head
function example we have not added constrained to the type parameter but we used it for the input and the output.
In that case, the type parameters helped us to create a “relationship” between the type of input and the output of the function.
Use type parameter at least twice or add a constraint to them.
We want our calcCartTotal
to be able to get cart
with many different types of items and OfCourse to be type safe.
We will add constraints to the type parameter,
First thing first: syntax. using the example from the official docs:
To apply constraints to the type parameters we use the keyword extends.
The extends keyword here means the type parameter can be any type that at least fulfills the requirement that comes after the extended keyword.
In this case, the type parameter can be any object
that has property length
of type number.
The Type before ‘extends’ keyword is more specific than the type after it.
A way to remember this is that in object-oriented programming, a class that extends another class is more specific.
Back to our pet store example (the customers are getting upset!),
These are the types for our different cart items:
We created the interface cartItem
which has a single property price:number
.
All the other interface extends it.
Interface extends means that the interface will get all the properties from the interface it extends.
So, Leash
will have price
from CartItem
and size
and color
from its own declaration.
We can stop here for a second and try to implement calcCartTotal
using a union instead
This code compiles and works perfectly, but there is a small problem, next week we will get a new delivery of Catnip.
Let’s try to add it to our cart:
This code breaks!
Type 'Catnip' is not assignable to type 'Leash | SqueakyToy'.
Type 'Catnip' is missing the following properties from type 'SqueakyToy': shape, color
As we did not specify it in cart
type in order for it to compile, we have to add it to the union like so:
However, we don’t want to update the type of cart
every time we have a new item in the store. That’s a big problem!
Luckily adding constraints to our type parameters will solve this problem.
Let’s create a generic constrained version of calcCartTotal
T extends CartItem
which means that the type parameter T
has to be an object type with price
property of type number
. Of course, it can have many more properties.
Let’s see the usage of our new function
In line 33 we see no compile errors as leash
and squeakyToy
has the property price
of type number
which satisfied the constrained we put on T
.
We created a function that can accept an array of any CartItem
, even items that do not exist in the store yet. Meaning we will not have to change cart
type any more.
Also, we made our friends on all four very happy.
Sum up
In this article we explored what Type Script Generics are.
We learned how to use Generics and the power of adding constraints.
I hope that by using these tools you will be able to write a more abstract and reusable code and of course understand others code better.
There is a lot more to learn about TS generics so stay tuned, in my next article we will learn about Generic Scope and Conditional Types.
In this article I have mainly used the Typescript Docs
And the wonderful Frontend Masters course
Keep on learning!