DEV Community

Simon Cleriot
Simon Cleriot

Posted on • Edited on

TypeScript method decorators example

Decorators are a stage 2 proposal for JavaScript already available as an experimental feature in TypeScript.

A decorator is a function that applies to a class, method, property or parameter and adds some logic to the latter. In other word, using a decorator is the same (but far simpler) as creating a new class that extends the target class and has a field pointing to it:

Wikipedia schema
Source: https://en.wikipedia.org/wiki/Decorator_pattern

You can have even have decorator factories that customize how the decorator is applied to a declaration (making it more easily reusable in different contexts).

In this example, we're going to use a method decorator factory to debounce a function.

Debounce concept

The perfect example for debounce is a realtime-search text input : in order to make a search, you send a request to the server. The naive implementation would send a request each time the text input changes, but it would overload the server and the view would have to wait for every request to finish before displaying the final results.

Debouncing delays the execution of a method for a fixed duration. If the method is called again while being stalled, the first method call is canceled. That way, the search component would only send one request to the server, when the user stops typing.

Debounce concept

Which ultimately looks like this:

Debounce example

TypeScript configuration

Decorators are still an experimental feature in TypeScript so you need to explicitly enable experimentalDecorators compiler option. Two possibilities there, depending on how you're building your project:

Command line:

tsc --target ES5 --experimentalDecorators
Enter fullscreen mode Exit fullscreen mode

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}
Enter fullscreen mode Exit fullscreen mode

Create the decorator

A decorator factory is declared as a normal TypeScript function :

function Debounce(wait:number, immediate:boolean=false) {
Enter fullscreen mode Exit fullscreen mode

wait is the millisecond delay.
immediate sets if we want to "call the function then stall for x milliseconds before allowing it to be called again", or "stall for x milliseconds then actually call the function".

The Debounce method is going to return the decorator function (that's why it's called a factory):

return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
Enter fullscreen mode Exit fullscreen mode

This function gets its three parameters from the element it's applied on (in our example it's going to be the method we want to debounce):

  • target references the element's class. It will be the constructor function for a static method or the prototype of the class for an instance member
  • propertyKey is the name of the element
  • descriptor is the Property Descriptor of the method, so we can alter it

The body of the decorator function looks like this:

var timeout:any;
var originalMethod = descriptor.value;
descriptor.value = function() {
    var context = this
    var args = arguments;
    var later = function() {
        timeout = null;
        if (!immediate) originalMethod.apply(context, args);
    };
    var callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) originalMethod.apply(context, args);
};
return descriptor;
Enter fullscreen mode Exit fullscreen mode

We store the original body of the method:

var originalMethod = descriptor.value;
Enter fullscreen mode Exit fullscreen mode

Then we change it by setting descriptor.value to a new function:

descriptor.value = function() {
Enter fullscreen mode Exit fullscreen mode

We then use timeouts to delay the method execution.

The value returned by our method decorator will override the original class method's descriptor.

In the end we have the following debounce decorator factory:

function Debounce(wait:number, immediate:boolean=false) {
    return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        var timeout:any;
        var originalMethod = descriptor.value;
        descriptor.value = function() {
            var context = this
            var args = arguments;
            var later = function() {
                timeout = null;
                if (!immediate) originalMethod.apply(context, args);
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) originalMethod.apply(context, args);
        };
        return descriptor;
    }
};
Enter fullscreen mode Exit fullscreen mode

Use it

We want to debounce the following search method (from a Vue.js component's class):

search(query: string) {
    axios.get(`users`, {
        params: {
            name: query
        }
    })
    .then(response => {
        // ...
    })
}
Enter fullscreen mode Exit fullscreen mode

We simply apply our previously defined decorator, with the right amount of time:

@Debounce(500)
search(query: string) {
    axios.get(`users`, {
        params: {
            name: query
        }
    })
    .then(response => {
        // ...
    })
}
Enter fullscreen mode Exit fullscreen mode

And that's it, the search method is going to be called only if no other search call is sent during 500ms.

You should definitely consider using Vue.js with TypeScript, they're a great match!
https://fr.vuejs.org/v2/guide/typescript.html

To dig deeper into TypeScript decorators:
https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841
http://www.typescriptlang.org/docs/handbook/decorators.html
http://www.sparkbit.pl/typescript-decorators/
https://cabbageapps.com/fell-love-js-decorators/

Originally published on my personal blog.

Top comments (2)

Collapse
 
17cupsofcoffee profile image
Joe Clay

This was a nice introduction to the new syntax! Really excited for decorators to finally become part of the JS spec (hopefully) soon.

One note - I believe you only need the emitDecoratorMetadata option if you want to use TypeScript's reflection API, which isn't currently part of the stage 2 proposal. For the examples you gave, I think turning on experimentalDecorators is all you need :)

Collapse
 
scleriot profile image
Simon Cleriot

You're absolutely right, I copied/pasted that config from one of my project. I'll update the article ;)