Level-Up Your TypeScript Game With Decorators and Transformers
TypeScript is an amazing programming language. It lets you write better code with most – if not all – mistakes caught at design time instead of blowing up in your face at runtime (still, give some love to your error handling, always 💖). It is a really mature and fully-fledged language that gets a lot of traction worldwide and amongst the engineers at Cloudsoft.
I would like to share with you 2 features that can enhance greatly your development workflow.
These are:
- a known-but-under-used feature of TypeScript: decorators.
- an unknown-and-not-really-public feature of TypeScript: transformers.
I’m also going to show you the really cool stuff that these features unlock when used jointly.
Alright let’s go!
TypeScript decorators, what are they?
If you are familiar with programming, there is a synonym that you probably already know: annotation. Decorators are not a TypeScript feature, but actually come from JavaScript, being a stage 2 proposal. They are at their core just JavaScript functions, which can be “hooked” to classes, methods, accessors, properties or parameters.
When transpiled by TypeScript, decorators wrap these resources to perform meta operations. For example, let's say you wish to initialise a couple of classes the same way, but you also need to override this initialization for a few of them. Writing a decorator to annotate your classes let you do this and be DRY:
Running this example will output:
Fueled Falcon9 with 100T. Ready to launch 🚀
Fueled Starship with 250T. Ready to launch 🚀
If you look at the transpiled JavaScript code, the constructor is wrapped by the decorator function which is how the `fuel` property is set. Because these methods are just wrappers, they are almost exclusively used at runtime. But what if I told you that you can also use decorators at compile time?
That’s right, you can perform arbitrary actions at compile time, based on decorators, and that’s where things get exciting! Spoiler alert: if you've used Angular, that’s exactly what happens behind the scenes with the decorators @component and @inject.
Behold TypeScript transformers
For folks who are familiar with Java, you can think of transformers as annotation processors. Although, these are more powerful – and cooler IMO – as they not only can generate content, but also access and update the AST of the file under processing.
While transformers are a perfectly valid concept for the TypeScript compiler, the API is not publicly exposed. So to use it, you will need your own wrapper around the 'tsc' compiler, a node module like 'ttypescript' or use 'webpack'.
A basic transformer is a recursive function that takes an AST node as parameter, and returns an AST node. The returned node can be the same node (for no modifications), a different one or `undefined` to remove the node from the AST.
For instance, the example below is an identity transformer which goes recursively into each node, and returns it directly.
But with a little bit of imagination, you can see how powerful that is.
Endless possibilities
So what can you do with that? Glad you ask.
The most striking example I made is for a big, real-life ServiceNow scoped app (i.e. ServiceNow term for a plugin) that we are developing and maintaining for a customer. Now, I already see people running away, screaming like they saw an army of zombie-ghosts at the mention of ServiceNow: don’t worry, I’m not going to talk about that. Well… not really anyway, but I have to share with you some context so you can understand why I came up with a technical solution that involves these 2 TypeScript features.
Context
I promise, I’ll keep this short. Instead of diving into the ServiceNow world, let me give you a 30,000 feet overview. When you develop a scoped app for ServiceNow, you have to use their “IDE” which is called “Studio”. A scoped app is comprised of “application files” which, most of the time, are JavaScript scripts with metadata attached. ServiceNow uses a custom Rhino engine to execute these scripts that is stuck between ES5 and ES6 specs, so most of the cool stuff that JavaScript came with in the past couple of years cannot be used out of the box or requires polyfills.
A scoped app can be version controlled, although because each application file is effectively a record on the ServiceNow DB, the actual files that end up on git are serialisation of these records, in the form of XML files.
Alright, are you still with me? Brilliant, and good news for you: that’s the end of the ServiceNow part 🥳
We started our dev journey in the world of scoped apps by using only studio. However we quickly came across hurdles, consequences of what I just explained above:
- pull requests were a nightmare: we had to compare serialised code + noise coming from DB record metadata (e.g. timestamp, ACLs, etc).
- there was no way to unit test our code.
- as devs, we didn’t get to use our IDE of choice, hence no inline help, no code completion, nothing.
What we did next is maybe a bit mad, but it was the most sensible way to fix the 3 points above: we created another git repo to store these scripts, and converted almost everything into TypeScript.
All development took place, and still takes place, in this “new” git repo. Our code is now well structured, much more robust, tested and we can enjoy the entire scope of development helpers that our IDE provides thanks to TypeScript.
However, working this way introduced a major drawback: for any change in a TypeScript file, the transpiled JavaScript version needs to be manually copy-pasted onto the right “application file” in “Studio”. It is not only tedious but excessively error-prone. Over time, we came up with a process to mitigate errors and catch them early, but the process remained manual and far from ideal.
Surely there must be a better way 🤔.
The technically-awesome way
I started my quest with the following questions:
What if we had a way of telling which “application file" to update from TypeScript?
What if it could be deployed automatically?
What would be necessary to achieve this?
You see where this is going, don’t you? At the expense of being obvious, I used TypeScript decorators and transformers to do the following:
- Each TypeScript file is annotated with a `@Deployable` decorator that takes the table name, column name as well as the ID of the record as parameters. This is to know what record to update and where.
- When compiling the project, a custom transformer looks for any `@Deployable` decorators, performs a quick validation to check everything is in order, then stores the path of the TypeScript and JavaScript files, along with the parameters from (1).
- At the end of the compilation, the object of (2) is serialised as JSON and written on disk into the output folder.
Note that the transformer looks for any `@Deployable` decorators. However, decorators can only be applied to classes, methods, properties and arguments. Our codebase also contains scripts that are IIFE functions which I also needed to deploy. The solution was to also look for `@Deployable` decorators in comments. When the transformer encounters one, it creates a `Node` on the fly and parses it as a regular node.
Also note that when the transformer encounters an annotation, it returns `undefined`. That effectively removes the decorator from the AST, meaning the transpiled version is the same as a TypeScript script without the decorator.
Then, with a bit of magic, and help from `yargs`, I created a small CLI tool within our project to encapsulate the logic above, and have everything nicely packaged. This means that from a single command, the tool will:
- Get the list of files that have changed, compared to the `master` branch.
- Compile the project, using my custom transformer + decorator.
- Read the output JSON and iterate through.
- For each entry, a query to the ServiceNow instance is made to update the record with the new transpiled version.
Conclusion
With a bit of creativity and less than 300 lines of code, TypeScript decorators and transformers changed an error-prone and long winded process into a very simple one. It’s fast, reliable and frankly super cool to have solved one of our biggest bottlenecks with what I would describe as an elegant solution.
Of course, this is not the only use case. You could use these to:
- Generate documentation.
- Generate model classes based on decorators.
- Initialise instances and behaviours.
- Perform static analysis.
- Etc.
Do you have any other ideas on how to use this? Can it solve something in your organisation? I would love to hear about your ideas!
Subscribe
Related Posts
-
News
Press Release: Unifii announces a strategic partnership with Cloudsoft to expand uptake of the AWS Service Catalog connectivity for ServiceNow customers
-
Press Release: Cloudsoft launches professional services for AWS Service Catalog Connector for ServiceNow
-
Technical
AWS Load Balancers & WAF: Availability vs Security with 'fail open'.