TypeScript Decorators by Example

One of coolest, yet least approachable features in TypeScript is the Decorator. We see Decorators implemented by the Angular Framework for classes like @Component, properties like @ViewChild, and methods like @HostListener, but have you ever considered building your own from scratch? They seem magical 🍄 in practice, but they are just JavaScript functions that allow us to annotate our code or hook into its behavior - this is known as Metaprogramming.

There are five ways to use decorators and we will look at examples of each one.

  • class declaration
  • property
  • method
  • parameter
  • accessor

Decorators are very good at creating abstractions - almost too good. While it is tempting to create a decorator for all of the things, they are best suited for stable logic that needs to be duplicated in many places.

Class Decorator

A class decorator makes it possible to intercept the constructor of class. They are called when the class is declared, not when a new instance is instantiated.

Side note - one of the most powerful characteristics of a decoractor is its ability to reflect metadata, but the casual user will rarely need this feature. It is more suitable for use in frameworks, like the Angular Compiler for example, that need to to analyze the codebase to build the final app bundle.

Example

Real World Use Case: When a class is decorated you have to be careful with inheritence because its decendents will not inherit the decorators. Let’s freeze the class to prevent inheritence completely.

file_type_ng_component_ts hook.component.ts
@Frozen
class IceCream {}

function Frozen(constructor: Function) {
  Object.freeze(constructor);
  Object.freeze(constructor.prototype);
}

console.log(Object.isFrozen(IceCream)); // true

class FroYo extends IceCream {} // error, cannot be extended

Property Decorator

All of the examples in this guide use Decorator Factories. This just means the decorator itself is wrapped in a function so we can pass custom arguments to it, i.e @Cool('stuff') Feel free to omit the outer function if you want to apply a decorator without arguments @Cool .

Property decorators can be extremly useful because they can listen to state changes on a class. To fully understand the next example, it helps to be familar with JavaScript PropertyDescriptors.

Example

Let’s override the flavor property to surround it in emojis. This allows us to set a regular string value, but run additional code on get/set as middleware, if you will.

file_type_ng_component_ts ice-cream.component.ts
export class IceCreamComponent {
  @Emoji()
  flavor = 'vanilla';
}


// Property Decorator
function Emoji() {
  return function(target: Object, key: string | symbol) {

    let val = target[key];

    const getter = () =>  {
        return val;
    };
    const setter = (next) => {
        console.log('updating flavor...');
        val = `🍦 ${next} 🍦`;
    };

    Object.defineProperty(target, key, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });

  };
}

Method Decorator

Method decoractors allow us override a method’s function, change its control flow, and execute additional code before/after it runs.

Example

The following decoractor will show a confirm message in the browser before executing the method. If the user clicks cancel, it will be bypassed. Notice how we have two decoractors stacked below - they will be applied from top to bottom.

file_type_ng_component_ts ice-cream.component.ts
export class IceCreamComponent {

  toppings = [];

  @Confirmable('Are you sure?')
  @Confirmable('Are you super, super sure? There is no going back!')
  addTopping(topping) {
    this.toppings.push(topping);
  }

}


// Method Decorator
function Confirmable(message: string) {
  return function (target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
    const original = descriptor.value;

      descriptor.value = function( ... args: any[]) {
          const allow = confirm(message);

          if (allow) {
            const result = original.apply(this, args);
            return result;
          } else {
            return null;
          }
    };

    return descriptor;
  };
}

React Hooks for Angular 🤯

You’ve probably heard that React Hooks are a game-changer for the web. Is there any chance Angular can catch up to produce code that is equally beautiful, succinct, and game-changing? Well, yes actually, and it has been able to do this from day one.

React hooks game changer results

UseState Property Decorator

In react, the useState hook provides you with a reactive variable count and a setter setCount.

file_type_reactjs hook.jsx
import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

We can achieve a similar results with a property decorator that will first define the count on the component - this is trival because Angular performs automatic change detection. We then use the name of this property to define a setter with the name of setCount. Usage looks like this:

file_type_ng_component_ts hook.component.ts
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <p>You clicked {{count.value}} times</p>
    <button (click)="setCount(count.value + 1)">Click Me</button>
  `,
})
export class HookComponent {
  @UseState(0) count; setCount;
}

And the decoractor implementation is just five lines of code. We just set an initial value, then find the cooresponding

function UseState(seed: any) {
  return function (target, key) {
    target[key] = seed;
    target[`set${key.replace(/^\w/, c => c.toUpperCase())}`] = (val) => target[key] = val;
  };
}

UseEffect Method Decorator

The effect hook hook simply consolidates the component lifecycle of componentDidMount and componentDidUpdate into a single callback.

file_type_reactjs hook.jsx
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

This is very easy to emulate with a method decorator because we can apply the function descriptor to Angular’s equivelent ngOnInit and ngAfterViewChecked lifecycle hooks.

file_type_ng_component_ts hook.component.ts
@Component(...)
export class AppComponent {
  @UseEffect()
  onEffect() {
    document.title = `You clicked ${this.count.value} times`;
  }
}


/// Implementation Details:

function UseEffect() {
  return function (target, key, descriptor) {
    target.ngOnInit = descriptor.value;
    target.ngAfterViewChecked = descriptor.value;
  };
}

Questions? Let's chat

Open Discord