Hello everyone! This post is based on Jean Sebastian's last Strapi Conf 2023 talk. I will link to the video at the end of the post.
JS is a backend engineer specializing in developer experience at Strapi. He also loves TypeScript.
Take it away, JS.
Hello everyone! I'm Jean Sebastian, and I'm excited to share some insights on how you can improve your developer experience, especially when dealing with applications with highly dynamic content structures using TypeScript. So, without further ado, let's dig in.
TypeScript is a powerful tool that enhances developers' experience with numerous functionalities. These include and are not limited to features ensuring type safety, offering improved IntelliSense, and simplifying navigation through complex codebases.
For instance,
Think of TypeScript as a referee in a game. It reads the rules, i.e., types and configurations, provided by developers and accordingly calls out warnings, errors and emits codes.
These combined make TypeScript a tremendous asset in improving productivity while reducing development costs by eliminating most running errors and codebase expiration struggles.
However, assuming TypeScript is a magic wand, transmuting JavaScript to TypeScript by a mere renaming operation would be naive.
TypeScript works very well regarding intellisense or type checking because it relies on the source of Truth, the types defined by the Developers.
Developers tend to know the application structures and models very well; for instance, someone developing an e-commerce website likely knows that they will need a model to represent a project, a brand, and a cart and also how to interconnect them by creating links and relation, in the same way, someone making a Blog know that they are dealing with post tags and authors.
Typescript is only the referee; it reads the rules written by the Developers.
Here in Strapi, we have an interesting challange. We don't know the content structure ahead of time or anything precise about our user applications model.
So we can't presume and generate types ahead of time since the possibilities will be endless, since, as a Strapi user, you can create any content types that your application needs for your specific use case.
Making shared types is a challenge. How will TypeScript work when the models are highly unpredictable?
First, let's look at an example of when we fetch data. The goal is to create a generic function that queries a given model to fetch its related content from the database.
While the above code is accurate from a Typescript perspective, it doesn't help us much when developing our application because here we have loose type checking, no auto-completion for the available uids, or any information about the values we can expect in the response.
It makes the developer experience a bit weird, and you will likely have to do a lot of casting to adapt the types to your project.
To fix this, we would need access to some kind of typescript database or registry of all the models that exist in our external application and their uids.
We can change our code to the following.
and this is where dynamic type registries come in.
Dynamic type Registries are regular interfaces declared in a shared module that the Strapi store would export.
While empty by default, we decided to leverage interface merging and augmentation abilities to allow your Strapi application or plugin to add new type definitions to those interfaces.
For instance, if you use an article model in your Strapi application, you can register it in the shared interface so that it can be picked up by Strapi when using its internal API.
Thus, we have a database of shared components, such as the models in our case, being used by internal apis while being populated and augmented by external actors.
In the end, this allows us to write generic APIs based on abstract models and entities on the Strapi side while giving the full power of typescript autocompletion and IntelliSense in user projects that extend the type registries.
Here's the design of what we've just explained; as you can see, Strapi exposes a module of shared Registries containing many interfaces.
The module and its interfaces are then extended by external actors such as the application or a plugin and can then be used by anyone to create custom APSs or just reference types.
While this solves the problem of having precise types and developing powerful typed APIs, this is not a very scalable solution.
Having to write complex types and augment manually many shared registries for a single application is just too much to ask, and in the end, it will only create frustration.
Unfortunately, this manual process is not scalable due to the complexities involved, leading to poor developer experience. But there's a solution - Automatic Type generation.
It consists of Strapi reading your application models to generate types that will then augment the dynamic registries.
But first, let's define our needs.
And should avoid string manipulation such as concatenation, etc., which led us to discover TypeScript Factory API.
The TypeScript factory API is a set of utils made to generate and manipulate TypeScript AST nodes. The nodes can then be used to emit definitions in a source file.
We transformed our models into typescript nodes using the factory API, created fake source file from the same definition, and then finally emitted typescript definition files containing the registry augmentation and related model types.
Here you can see what happens when we manually trigger type generation as it creates types for the Strapi application content types and components.
And here, we can see a part of what's being emitted.
We can observe the store module augmentation and the component registry being extended with different models.
In the end, we found a workaround to enjoy every benefit of TypeScript when dealing with highly dynamic content structures.
We can see here that our previous example worked perfectly with the generated types and augmented registries.
I hope you enjoyed this post by JS, and we will see you next time.