Having fun with Angular and Typescript Transformers

Never think of RxJs Subscriptions again

Christian Janker
Angular In Depth
5 min readJun 6, 2019

--

AngularInDepth is moving away from Medium. This article, its updates and more recent articles are hosted on the new platform inDepth.dev

Do you know the burden of handling your RxJs subscriptions manually? Did you ever forget one? Did you ever run into the problem that you thought you would be safe with the use of the async pipe in the template and then after some time a new requirement comes in and you realize that you need a subscribe call in your component class as well? This may be a smell for a bad design of some of your components, but let's be honest: Sometimes it is the right way in order to get things done.

Wouldn't it be nice if we never have to think about subscriptions again?

No more manual handling of subscription arrays.
No more takeUntil(this.destroyed$)
No more subscription.add()
No more pain and fear ;)

We can achieve that with the help of Typescript Transformers at build time.

Before we go into ecstasy about this, which I certainly did, I have to point out that there are disadvantages with black magic code generation like the one I am presenting here. Sometimes it is even wrong to unsubscribe. So be aware, that the following example primarily has a learning purpose.

What are typescript transformers?

Typescript Transformers allow us to hook into the compilation process of Typescript and transform the Abstract Syntax Tree (AST) that is produced.

This allows us to change the existing code at compile time. In the following sections of this post we will, for example, use it to:

  • find all classes with a @Component() decorator
  • find all calls to subscribe() of RxJs
  • generate methods like ngOnDestroy
  • extend the body of existing methods

This is a very powerful process and it is heavily used by the Angular compilation process itself.

To get a feeling for an AST it helps to take a look at an example at astexplorer.net. On the left side of this explorer you can see the source of the component class TestComponent and on the right side its AST representation. You can change the code on the left and the AST is immediately updated.
This tool will become incredibly helpful later when we want to find out how to write the transformer code to extend the existing source or generate new parts.

But first, let’s have a look at a basic skeleton of a transformer:

In our example the function simpleTransformerFactory is the TransformerFactory, which returns a Transformer.

The typings of Typescript itself (the second code snippet) show that a transformer itself is just a function that takes a Node and returns a Node.

In the snippet above, where we just log every class name we find, we walk through the Typescript AST (Abstract Syntax Tree) with the so-called visitor pattern where every node of the AST is being visited.

A node could be a:

  • CallExpression like this.click$.subscribe()
  • BinaryExpression like this.subscription = this.click$.subscribe()
  • ClassDeclaration like class Foo {}
  • ImportDelcaration like import {Component} from ‘@angular/core’
  • VariableStatement like const a = 1 + 2
  • MethodDeclaration
  • ….

Generating the unsubscribe code

Our goal is that before all the transformers of Angular itself are running our custom transformer is executed. Its task is to find all subscribe calls in components and generate the code to automatically unsubscribe from them in the ngOnDestroy method.

Thanks to Manfred Steyer and David Kingma there is not that much work to do to achieve that.

In order to be able to inject our custom transformer into the transformation process of an Angular-CLI project, we can use the ngx-build-plus library and its plugin feature. In a plugin, we can access the AngularCompilerPlugin and add our transformer to the “private” transformers array.

The ngx-build-plus plugin. Thx to David Kingma

The next code block shows the main part of the Typescript Transformer, that is responsible for generating the unsubscribe calls. It is not the whole source of the transformer but it shows the most important steps.

Step 1)
Make sure we are within a component class. If we are, we have to remember that in a context variable withinComponent because we just want to enhance subscribe() calls that are made within a component.

Step 2)
We then immediately call ts.visitEachChildNode() to find all subscriptions made in this component.

Step 3)
When we find a subscribe() expression within a component we wrap it with a this.subscriptions.push(subsribe-expression) call.

Step 4)
If there was a subscribe expression within the child nodes of the component, we can add the subscriptions array.

Step 5)
Then we try to find the ngOnDestroy method and create it if there isn’t one.

Step6)
At last, we extend the body of the ngOnDestroy method with the unsubscribe calls: this.subscriptions.forEach(s => s.unsubscribe())

Full Source

Following is the full source of the unsubscribe transformer. I don’t want to go into the detail of the Typescript Compiler API itself, because it would be definitely too much for the scope of this post.

My approach basically was a trial and error one. Pasting some existing source code into astexplorer.net and then trying to create the AST programmatically.

I will share some useful links to other transformer posts in the summary section.

To run the whole thing we first have to execute following command in our project root:

  • tsc --skipLibCheck --module umd to compile the transformer.ts and the plugins.tsfile
  • then we can run ng build --plugin ~dist/out-tsc/plugins.js to execute the build pipeline from Angular with our added plugin. The result of this process can be viewed in the main.js file in the dist folder.
  • optionally you can serve it with ng serve --plugin ~dist/out-tsc/plugins.js

With a given component, in which we intentionally don’t handle our subscriptions:

Following code is generated after the whole transformation and build process of Angular:

Can you spot the handled subscriptions? :)

Summary

I think there is a reason, why the Angular team keeps its transformer API private. It is a clear sign that we should not extend it on a regular basis.
The unsubscribe transformer is a nice idea but it also shows that the whole thing gets complex very easily because we would have to consider a bunch of edge cases to come up with a bulletproof solution.

Some ideas pop into my mind though:
We could write a custom JAM Stack transformer, which executes http requests at build time.
Or we could leverage the Typescript Compiler API to generate the TestBedstatement for our unit tests with all necessary dependencies already included.

Further information:

Converting Typescript decorators into static code by Craig Spence
This is a very interesting post, where the access to the AST is simplified by the great library tsquery, which you should definitely check out if you want to write custom typescript linters or transformers.

Do you know how Angular transforms your code? by Alexey Zuev
This post explains in detail how Angular itself uses Typescript Transformers at build time. Very informative and we can learn a lot from the transformers written by the Angular team.

Custom Typescript Transformer with Angular by David Kingma
In my opinion an underrated post, which shows how we can write custom transformers and integrate them into the Angular CLI build.

Using the Compiler API
Documentation of the Typescript Compiler

Have a nice day. The sun is shining in Austria. Stay tuned :)

Github Repo of the example above.

Follow me on Twitter.

--

--