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:
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.
Which ultimately looks like this:
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
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Create the decorator
A decorator factory is declared as a normal TypeScript function :
function Debounce(wait:number, immediate:boolean=false) {
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) {
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;
We store the original body of the method:
var originalMethod = descriptor.value;
Then we change it by setting descriptor.value
to a new function:
descriptor.value = function() {
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;
}
};
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 => {
// ...
})
}
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 => {
// ...
})
}
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)
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 onexperimentalDecorators
is all you need :)You're absolutely right, I copied/pasted that config from one of my project. I'll update the article ;)