My first TS custom decorator

Background

Was reading some nestjs related documentation and found the custom decorator portion quite interesting. So decided to write one myself.

Decorator vs Annotation

Syntax-wise decorator in Typescript is quite similar to the annotation in Java where we can assign metadata information to class/field/method. However under the hood, they are quite different. One of the major difference I think is the support at runtime.

Java Annotation

In Java, Annotation has real run time support where we can get the annotation class by calling something  like

applicationContext.getBeansWithAnnotation(someClazz)

then use the native

clazz.getAnnotation(MyAnnoClass.class)

to get the annotation instance and then do processing accordingly.

TS Decorator

In TS, as it will eventually be transpiled to js, all the type info and other standard non-js stuff are wiped. As a result, it actually functioned via high order function. So basically the decorator itself is just a function which accepts the underlying function which will be called inside sometime, so that we can do manipulations before after or change param etc.
The signature is something like

function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor)

where the arguments point to different things when you annotate in class/field/method. This SO answer has good summary on what they are under different context.

Implementation

The one I plan to write is a common function we used -> Retry where any time the function throws error, it will re-run until reaches the retries count assigned in the decorator.

import * as util from 'util';

/**
 * retry decorator which is nothing but a high order function wrapper
 *
 * @param options the 'RetryOptions'
 */
export function Retry(options: RetryOptions): Function {
  /**
   * target: The prototype of the class (Object)
   * propertyKey: The name of the method (string | symbol).
   * descriptor: A TypedPropertyDescriptor — see the type, leveraging the Object.defineProperty under the hood.
   *
   * NOTE: It's very important here we do not use arrow function otherwise 'this' will be messed up due
   * to the nature how arrow function defines this inside.
   *
   */
  return function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) {
    const originalFn: Function = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      try {
        return await retryAsync.apply(this, [originalFn, args, options.retries, options.backOff]);
      } catch (e) {
        e.message = `Failed for '${propertyKey}' for ${options.retries} times.`;
        throw e;
      }
    };
    return descriptor;
  };
}

async function retryAsync(fn: Function, args: any[], retries: number, backOff?: number): Promise {
  try {
    return await fn.apply(this, args);
  } catch {
    if (--retries >= 0) {
      backOff && await sleep(backOff);
      return retryAsync.apply(this, [fn, args, retries, backOff]);
    } else {
      throw new Error(`Failed after Retries`);
    }
  }
}

const sleep: Function = util.promisify(setTimeout);

export interface RetryOptions {
  retries: number;
  backOff?: number;
}

Usage

The usage could be something like

  @Retry({ retries: 2, backOff: 2000 })
  private async retrieveSaml(): Promise {
    return await SamlRetriever.getSAMLPuppeteer(this);
  }

References:
1. This article explains the details of decorators on class/field/argument/method.

2. This article explains the real usage detail on Angular decorators.

3. This one explains nicely on decorator factory and composition.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s