Why We Chose Typescript & The Advantages of Generics
One of the perks of working at Unibuddy is that each squad has autonomy over their product’s design and implementation decisions. We’ve moved away from a monolithic architecture and are working towards microservices, this facilitates engineers specifically selecting the best tools they need for the job.
In this blog, I’ll discuss why we’ve chosen Typescript and cover the common advantages that it offers including type safety and the availability of generics, and then we’ll take a look at a Typescript generics example.
So why Typescript?
Typescript is a superset of Javascript which transpiles to Javascript. It implements static typing, which essentially means the code gets checked for certain errors before runtime. Static typing offers a few more advantages than simply checking for mistakes before they crash your application. Type inference means that an IDE knows what to expect and can offer predictive options. In other words, more engineers can work on the service and commit code with more confidence and less cognitive load of having to know the intricacies of the value types.
Generics takes this a step further and allows types, interfaces, and classes to act as parameters. This allows us to reuse code for different use cases where the input parameter varies but the implementation is reusable. So what do generics look like? A common convention is to use T
which denotes the unknown variable. A quick example is type Example<T> = T | T[]
where <T>
defines the generic type and T | T[]
denotes the form the generic could be passed in with.
So when would we use this?
Outlined below is an instance where we have written code that could benefit from using generics. In this example, we are implementing a new CacheService
that will be used by both a UserService
and a UniversityService
to cache User
and University
objects respectively.
When we first created our CacheService
it was made as a simple service that interacts with a third-party cache.
This service works fine but we’ve used the type any
twice. The Typescript any type can be assigned to literally any variable and it doesn’t offer us much security on what is being saved in our cache. The initial implementation of the CacheService
in the UserService
and UniversityService
is outlined below.
In this instance, keeping the cache generic has the advantage of allowing both our user and university services to implement it, and call the get
and set
methods, but there is no specification on the type of object we are saving to the cache.
What if we created two separate cache services?
An alternative implementation would be to specify that only the User
or University
type can be passed to the cache. To do this, we could create a UserCacheService
and a UniversityCacheService
.
Although we have improved the security of what we are saving to the cache, we have duplicated the amount of code we are writing. One of the advantages of Typescript generics is that it allows us to reduce this boilerplate code.
So let’s implement some generics.
We define a service to be of generic type T
by adding <T>
after the class name. Notice how the return type of the get method and the data we save in the cache are also of type T
.
We specify the unknown variable when we inject the CacheService
in the UserService
and UniversityService
constructors. You can see that the data in the set method is now specified, as is the return value from the get method.
This ensures that when we implement the cache service methods, we must specify the correct type, if we were to populate the set method in the UserService
with an object that did not match the User
type, then we would see a compile-time error (one of the great advantages of Typescript over Javascript).
We have now successfully reduced the amount of code we need to write whilst specifying what can be saved in the cache by using Typescript generics.
Could we further increase type safety with generics?
A way to take this a step further is to use inheritance with generics to specify what we expect our cache object to look like. For example, we may want to assert that each object in the cache has a name or creation date. The way we achieve this is with an interface (it should be noted that as of TS 2.7 a type can be used throughout the code examples in place of an interface and I highly recommend checking out this Stack Overflow post on the difference), which forms a syntactic contract that ensures the correct fields are present. We simply need to update the generic type to extend an interface with the required and/or optional fields we want.
Now any service that implements the CacheService
must implement a type that conforms to the CacheObject
. In theUserService
below we have implemented a new UserCacheObject
in place of the previously used User
object.
The UserCacheObject
implements the CacheObject
which ensures the correct fields are present.
With just a little bit more code we now have the great advantage of ensuring the correct information required is stored in the cache. The same method could be applied to the UniversityService
and any other service/object in future that needs to be stored in the cache.
There we have it, a very simple use case for generics that increases the safety of what data we store in our cache whilst reducing the amount of code written.
And that’s it!
This has been a relatively simple introduction to the advantages of Typescript with generics, and there are many more avenues you can explore with it. Primarily, generics offer us more security with less code and the added benefit of a sanity check before runtime.