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.
- 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?
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 🚀
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.
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.
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.
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.
- 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.
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.
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!