Lazar
Ljubenović

TypeScript Decorators: Usage Examples

The traditional way

We start from the traditional way of doing this so you can get the idea of what we’re trying to do. All we have is a class Circle with a single property radius. We pass the desired radius into its constructor when creating a new instance.

class Circle {
  public radius: number;
  constructor(radius: number) {
    this.radius = radius;
  }
}

Since you (probably) don’t want negative radii in your circles, a good thing to do is to throw an exception when you attempt to do so.

For this reason, we’ll create a getter and setter for radius, where we’re gonna check that everything is good to go.

class Circle {
  private _radius: number;

  public get radius() {
    return this._radius;
  }

  public set radius(newVal: number) {
    if (newVal < 0) {
      throw new Error(`Radius must be > 0`);
    }
    this._radius = newVal;
  }
}

This obviously works. But then we add a center, outline color, fill color… and the list just grows. Code would be cluttered with trivial assertions all over the place.

Using a Decorator

Let’s take a look at PropertyDecorator declaration in TypeScript source code.

declare type PropertyDecorator =
  (target: Object, propertyKey: string | symbol) => void

A PropertyDecorator is a function which takes two parameters. Let’s figure out what these are by creating a function Ensure and just logging these parameters. We decorate the radius property with this decorator.

function Ensure(target: Object, propertyKey: string) {
  console.log(target);
  console.log(propertyKey);
}

class Circle {
  @Ensure public radius: number;
  constructor(radius) { this.radius = radius; }
}

As it turns out, target is an Object with a constructor Circle and target is just a string 'radius', the name of the decorated property.

A nice thing that property decorators allow us is to access the prototype of the class though target. So if we add target['foo'] = 'bar' in our Ensure function, we can see that we can run things like new Circle().foo, and it will give us bar.

Note, however, that bar is not “own property” of Circle. If you look up in developer tools, you can see that it’s actually listed under __proto__.

What we want to do is create a setter which will do the check for us, and this can be done using Object.defineProperty.

function Ensure(target: Object, propertyKey: string) {
  let val = this[propertyKey];

  function get() { return val; }
  function set(newVal) {
    if (newVal < 0) { throw new Error('Must be > 0.'); }
    val = newVal;
  }

  Object.defineProperty(target, propertyKey, {get, set});
}

We need to get the value of this[propertyKey] (this is actually this.radius when applied to radius property) to avoid circular definition of the getter — if we just return this[propertyKey], that would call the getter which would return the this[propertyKey], which would call the getter…

We also take advantage of ES2015 Object literal shorthand in the last line where we don’t write {get: get, set: set} but the equivalent {get, set}.

The rest is pretty straight-forward.

let circle = new Circle(-10);

Now when we run this, we get a nice error telling us that radius “Must be > 0”.

Factory Pattern

We don’t want a new decorator for each type of assertion we’re making with the properties. That would kinda ruin the whole idea. We might want to ensure that a value is between 0 and 255 for a color channel, for example.

There’s a common pattern in JavaScript where you write a function which serves to return another function, but also to provide a closure over the parameters given to it: the factory pattern. It’s called so because it’s actually a factory of similar functions which you can produce when needed.

How does that relate to our problem with decorators? Well, our decorator has to be a function with a specific signature (just like in the map example). If we want to change the “0” as the threshold for accepting or rejecting a value, we’d need to spawn a function using a factory.

So, we could do the following.

const Ensure = t =>
  function(target: Object, propertyKey: string) {
    let val = this[propertyKey];

    function get() { return val; }
    function set(newVal) {
      if (newVal < t) { throw new Error(`Must be > ${t}.`); }
      val = newVal;
    }

    Object.defineProperty(target, propertyKey, {get, set});
  }

Now we apply the decorator like this.

class Circle() {
  @Ensure(0) public radius: number;
}

class LargeCircle() {
  @Ensure(10) public radius: number;
}

That’s why less boilerplate code!

But what if we want to ensure that a number is between two numbers? Sure, our factory could take two parameters, so we could call it like @Ensure(0, 255) public red for a Color class.

But each time we need another type of assertion, we’d need to add more parameters. Besides, what if we want strings that begin with a capital letter for names? We could get always create a new decorator, of course, but that’s gonna get wild after a while.

Instead, we could pass a function to the decorator. Instead of hard-coding the newVal < t part in the Ensure decorator, what we can do it pass that whole function. We call the function f in the following snippet.

const Ensure = (f: ((val: any) => any)) =>
  function(target: Object, propertyKey: string) {
    let val = this[propertyKey];

    function get() { return val; }
    function set(newVal) {
      if (f(newVal)) { throw new Error(`Must satisfy f.`); }
      val = newVal;
    }

  Object.defineProperty(target, propertyKey, {get, set});
}

Now you have complete control over the assertions.

class Circle {
  @Ensure(x => x > 0) public radius: number;
}

class Color() {
  @Ensure(x => x >= 0 && x <= 255) public red: number;
  @Ensure(x => x >= 0 && x <= 1) public alpha: number;
}

If we feel we often have the “between a and b” assertions, we could use the factory pattern again. You could generate the functions similar to the ones in the Color class above by using a factory which accepts a and b as parameters.

const between = (a, b) =>
  x => (x >= a) && (x <= b);

The factory function between spawns a function which takes a number and returns true if it is “between” the numbers we provided to the factory. The Color class is now even more readable.

class Color {
  @Ensure(between(0, 255)) public red: number;
  @Ensure(between(0, 1)) public alpha: number;
}

As another example, we might want to make sure that a person’s name and surname start with a capital letter, and to validate an email with a regex.

Homework: Better Error Messages

If you’ve been paying super-close attention, you’ve noticed that we’d lost a nice error message somewhere along the way. Being given a generic error Must satisfy f kind of sucks.

So try this out. Give the decorator factory an optional errorMsg parameter. This is what it might work like:

const color = new Color();

color.red = 500;
// "The red channel must be between 0 and 255 (given value: 500)."

color.alpha = -1;
// "The alpha channel must be between 0 and 1 (given value: -1)."

And don’t stop there! Explore and tell me what you’ve found.