Custom TypeScript Decorators Using RxJS and Angular Services

TJ Seaman
Capital One Tech
Published in
9 min readJun 18, 2019

If you have ever worked with Angular before, you may be familiar with TypeScript decorators and how they are used inside of the framework. From @Component to @Injectable, @Input, and @Output, decorators are a key characteristic that makes Angular development easier. Outside of the Angular framework, developers have also innovated many additional decorators that have great use cases and utility. @AutoUnsubscribe (via ngx-auto-unsubscribe), method deprecation decorators, and logging are a few examples of incredibly useful decorators that make our code cleaner by packaging up logic that can be reused throughout your application in a utilitarian fashion. Today, I want to introduce one more way that we can utilize decorators with RxJS and Angular’s async pipe to create a highly reusable custom decorator.

Disclaimer — Currently, TypeScript decorators is on its second proposal. Decorators are still considered experimental, and require opt-in from developers. With that said, keep in mind that the specifications for this language feature have not been implemented natively in ECMAScript yet and is subject to change at any point in the future. For the time being, “The decorators champion group would recommend continuing to use Babel “legacy” decorators or TypeScript “experimental” decorators.” — TC-39 Proposal

With that said, I want to introduce and explore an interesting way in which we can utilize decorators in our applications today. Let’s jump into it!

The Use Case

Many companies like to test their user interfaces with A/B tests for the best possible user experience. Typically these tests require some user context, an API call and response, and some updates to the DOM to render the appropriate experience.

In our scenario, we’re going to assume the following is true about any given user:

  • The user has a unique ID
  • The user has a preferred language
  • The user is eligible for special features

The third-party A/B library provides a function that requires user context as a parameter in order to make an HTTP call. This call determines what experience should be returned for the user and thus reflected on the page. More specifically, this function can take in a set of parameters (e.g the user context of ID, language, etc.), builds those arguments into the header as query string parameters, then makes an HTTP call that is returned as a promise. Once we’ve received the API response, we can convert the promise to an observable, then subscribe to it to grab our data and manipulate the DOM to display that specific experience.

The Set-Up

All right! Now that the use case is set-up, let’s see some pseudo code of what that third-party library function could look like. Afterwards, let’s wire it up with an Angular service to consume the library display the response in a component with some basic templating.

Third-Party Library

Below will demonstrate a basic example of a method we could expect from a third-party A/B testing library:

/ab-test-frameworks.ts
/models/experience.interface.ts

Keep in mind that the above is pseudo code. This is meant to demonstrate the usage of a third-party library that makes an XHR call and returns the request as a Promise. We will assume that this library will always return a JSON object that has data that we can leverage in our code. For example, we could receive:

{
experience: “A”,
. . .
}

The A/B Test Service

Now that we have an idea of how the third-party library works, let’s create a service that can be injected into any component. This service will be responsible for being the single point where the A/B function will be consumed. I’ll explain later why there are some additional benefits for creating a wrapper service for the consumption of the third-party A/B library function (hint: we can use it in our custom decorator).

/ab-test.service.ts
/models/user.interface.ts

In our service, we simply import the function that is provided by the third-party library and create a method in our service class that wraps the Promise in a RxJS provided method called of. Since we know that the promise will have a response of type Experience, we can define our method’s return type as Observable<Experience>.

Cool! Let’s use our new service in a component.

Create Demo Component

Now, let’s also define our basic A/B test component with the template and dependency inject the ABTestFrameworkService.

/my-component.component.ts (v1.0.0)
/my-component.component.html (v1.0.0)

At this point, we can easily call the service with some defined user context to get a JSON payload returned from our third-party library and render it in our DOM.

/my-component.component.ts (v1.1.0)
/my-component.component.html (v.1.1.0)

Now that we are subscribing to an observable, we need to make sure we handle our subscription when the component is destroyed. There are many different approaches to unsubscribing (i.e. Subsink, @AutoUnsubscribe, etc.), but let’s keep it simple for now.

/my-component.component.ts (v1.1.1)

Up until now, everything seems like business as usual. The approach to unsubscribing is perfectly valid, but what if we wanted to do it in a more Angular, ergonomic way?

One feature that may have come to mind is Angular’s async pipe. This pipe automatically handles subscriptions and garbage cleanup when the component is initialized and destroyed, respectively. One approach to utilizing the async pipe would be assigning the getUserExperience$() to a public class member. This would return an observable of our experience data to this class member, and then we can async pipe it in the template. This is not bad, but this requires us to dependency inject the service in the component’s constructor, then wire up everything between the template and component. There’s nothing wrong with this approach, but I think we can take it a step further by packaging up this functionality into a decorator.

The Decorator

If you’re reading this article, you probably have an idea of what a decorator is, but let’s refresh our memories really fast:

“A decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter.” — TypeScript Lang

There are some major benefits to using TypeScript decorators, so let’s highlight a few:

  • Decorators allow us to extract a block of logic that can then be easily reused throughout an application — This is often referred to as aspect-oriented programming (AOP), which generally adds additional behavior to existing code without modifying the code itself. In other words, you are coding your code (not to get too meta).
  • Decorators can modify classes, methods, members, accessors, and properties — Throughout the Angular framework, a variety of decorators are used such as @NgModule, @Component, @Injectable, @Input, @Output, etc. These decorators handle the heavy lifting of adding class metadata and manipulating class methods/members, among other things that we would otherwise have to do manually.
  • Decorator factories are invoked at definition time — If there are multiple decorators for a given TypeScript declaration, the expressions for each decorator are evaluated from top to bottom, and the results of the expressions are called as functions from bottom to top. Additionally, decorators are invoked at run-time of the TypeScript declaration. This may prove to be a limitation for external asynchronous events, which I’ll cover in a later section.

To put it simply (and I’m going to get a little meta on you here), think of decorators as a way to program your code to behave in a specific, repeatable fashion. That’s right, we are coding our code.

Cool stuff! Now that we know we can extend a piece of code’s behavior by decorating a TypeScript declaration, let’s use that in conjunction with our ABTestFrameworkService to set-up a basic custom property decorator that binds some value to the class member that I will use in my component.

/ab-test.decorator.ts (v1.0.0)

It’s as easy as that! We are simply invoking a factory function that returns a function expression. This grabs a reference to what it’s decorating (in our case, it’s a class member), and we are replacing the target’s value with “newValue”. This is useful because now we can bind ABTestFrameworkService.getUserExperience$() to any class property. But first, we need to grab a reference to our service in our decorator factory. How might we do that?

To do this, we need to expose Angular’s injector in the feature module that service is being consumed from. We can grab a reference of our service by defining a static class member and set a reference of the injector to that via the module’s constructor.

/my-feature.module.ts

From here, we can import our feature module into our decorator and grab a reference to our service using injector.get(). After we have a safe reference to the service, we can bind the method that returns the observable of our AB test data.

/ab-test.decorator.ts (v1.1.0)

Perfect! We have successfully bound the observable of experience data to our class member. You may be wondering about the Type import at the top. Don’t worry about it too much. This is a helper for Angular’s injector.get() to accurately return the correct reference of the service that is registered in the Injector. We are missing one last thing: providing the user context to the service method through our decorator. Let’s do that now.

/ab-test.decorator.ts (v1.1.1)

Now that we’ve defined our decorator, let’s go refactor our component to use it instead of our service.

/my-component.component.ts (v2.0.0)

Boom! We just eliminated a lot of boilerplate that we would have normally had to set up (seen in v1.1.1 of our component). We can remove both ngOnInit and ngOnDestroy life cycle hooks and all the respective code just to handle subscribing and unsubscribing. Now, we can simply async pipe response$ in our template. You’ve probably noticed that the way that we used this decorator seems similar to the usage of the @Component decorator. Let’s update our HTML template.

/my-component.component.html (v2.0.0)

One Caveat For Custom Decorators

In our use case, there may be a scenario where we need to determine some extra user context after the class has been initialized. Since decorators are executed at runtime during class initialization. There isn’t an opportunity to resolve any asynchronous events (i.e. resolving API calls, waiting for user interaction, etc.) before the decorator factories are evaluated and resolved. For this use case, we can inject the service, resolve any asynchronous calls, then invoke our service call with that additional context. It could look something like this.

/my-component.component.ts (resolve async)

While we aren’t able to use our decorator, our service is flexible enough to accommodate this type of A/B test scenario. Unfortunately, we lose the luxury of the async pipe, so we’ll have to handle our subscription responsibly with this approach.

The Wrap-Up

TypeScript decorators can be an incredibly powerful approach to encapsulating and applying repeatable logic in a clean, utilitarian way without cluttering our day-to-day Angular code. In our specific use case, we gain the benefit of Angular’s async pipe that can eliminate the need for including other library dependencies for subscription strategies with the exception of unresolved, external asynchronous events. I want to encourage everyone to give TypeScript decorators a shot and to explore other use cases that can be used throughout your application. Cheers!

P.S., if you were wondering what VS Code theme I was using throughout this article, check out Cobalt2. It’s really great for your eyes!

DISCLOSURE STATEMENT: © 2019 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.

--

--

TJ Seaman
Capital One Tech

Capital One Full-Stack Software Engineer, Angular enthusiast, NativeScript dabbler, database purist, fluent in Japanese, and a huge gamer.