Back to Course

Learn TypeScript

0% Complete
0/0 Steps
Lesson 20 of 25
In Progress

Decorators in Typescript

Decorators are a powerful feature in Typescript that allow you to modify the behavior of a class, method, or property at runtime. In this article, we will explore the basics of decorators and how they work in Typescript.

What are Decorators?

Decorators are functions that are used to add additional behavior to a class, method, or property. They are called “decorators” because they decorate or modify the target class, method, or property with additional functionality.

Decorators are implemented as functions, and they take the target class, method, or property as their first parameter. For example, here is the syntax for a class decorator:

function myDecorator(target: Function) {
  // Decorator logic goes here
}

And here is the syntax for a method decorator:

function myDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  // Decorator logic goes here
}

And here is the syntax for a property decorator:

function myDecorator(target: Object, propertyKey: string) {
  // Decorator logic goes here
}

How to Use Decorators in Typescript

To use a decorator in Typescript, you simply need to add the decorator function to the class, method, or property that you want to decorate. For example, here is how you would use a class decorator:

@myDecorator
class MyClass {
  // Class logic goes here
}

And here is how you would use a method decorator:

class MyClass {
  @myDecorator
  myMethod() {
    // Method logic goes here
  }
}

And here is how you would use a property decorator:

class MyClass {
  @myDecorator
  myProperty: string;
}

Decorator Example: Logging Method Calls

To demonstrate how decorators work in Typescript, let’s look at an example of a decorator that logs the name and arguments of a method every time it is called.

Here is how you could define the logMethod decorator:

function logMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling method ${propertyKey} with args: ${args}`);
    return originalMethod.apply(this, args);
  }
}

This decorator function takes three arguments:

  • target: The prototype of the class that the decorated method belongs to.
  • propertyKey: The name of the decorated method.
  • descriptor: The property descriptor of the decorated method.

The decorator function first stores the original method in a variable, and then replaces it with a new function that logs the method name and arguments before calling the original method.

To use this decorator, you would apply it to a method like this:

class MyClass {
  @logMethod
  myMethod(arg1: string, arg2: number) {
    console.log(`Inside myMethod with args: ${arg1}, ${arg2}`);
  }
}

Now, every time you call myMethod, it will log the name and arguments of the method:

const myClass = new MyClass();
myClass.myMethod('hello', 123);
// Output: "Calling method myMethod with args: hello, 123"

You can also use decorators with getters and setters in the same way:

class MyClass {
  private _myProp: string;

  @logMethod
  get myProp() {
    return this._myProp;
  }

  @logMethod
  set myProp(value: string) {
    this._myProp = value;
  }
}

const myClass = new MyClass();
myClass.myProp = 'hello';
console.log(myClass.myProp);
// Output: "Calling method get myProp with args: "
// Output: "Calling method set myProp with args: hello"

Note that the decorator function is called with the get or set keyword, depending on which accessor is being decorated.In this example, we saw how to use a decorator to log the name and arguments of a method every time it is called.

Decorator Factories

In addition to standalone decorators, Typescript also allows you to create decorator factories. A decorator factory is a function that returns a decorator function. This can be useful when you want to pass parameters to your decorator.

For example, here is how you could create a decorator factory that logs the name of a method along with the time it takes to execute:

function timeMethod(logMessage: string) {
  return function(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
      console.time(logMessage);
      const result = originalMethod.apply(this, args);
      console.timeEnd(logMessage);
      return result;
    }
  }
}

To use this decorator factory, you would call it with the desired log message as its argument:

class MyClass {
  @timeMethod('MyMethod')
  myMethod(arg1: string, arg2: number) {
    console.log(`Inside myMethod with args: ${arg1}, ${arg2}`);
  }
}

This will log the time it takes to execute myMethod along with the message “MyMethod”.

Decorator Composition

In addition to standalone decorators and decorator factories, Typescript also allows you to compose decorators by applying multiple decorators to a single class, method, or property.

For example, you could apply both the logMethod and timeMethod decorators to the myMethod method like this:

class MyClass {
  @logMethod
  @timeMethod('MyMethod')
  myMethod(arg1: string, arg2: number) {
    console.log(`Inside myMethod with args: ${arg1}, ${arg2}`);
  }
}

This would log the time it takes to execute myMethod along with a message, as well as log a message every time myMethod is called.

Conclusion

Decorators are a powerful feature in Typescript that allow you to modify the behavior of a class, method, or property at runtime. They can be used to add additional functionality to your code, and they are easy to use and compose. In this article, we covered the basics of decorators and how they work in Typescript.

Exercises

To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.

Create a decorator function called logProperty that logs the value of a property every time it is accessed or modified.

Here is an example of how you could use the decorator:

class MyClass {
  @logProperty
  myProp: string;
}

const myClass = new MyClass();
myClass.myProp = 'hello';
console.log(myClass.myProp);

Output:

Setting property myProp to value: hello
Getting property myProp with value: hello

Solution:

function logProperty(target: Object, propertyKey: string) {
  let value: any;
  const getter = () => {
    console.log(`Getting property ${propertyKey} with value: ${value}`);
    return value;
  };
  const setter = (newValue: any) => {
    console.log(`Setting property ${propertyKey} to value: ${newValue}`);
    value = newValue;
  };
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
  });
}

Create a decorator function called readonly that makes a property read-only, meaning it can only be accessed and not modified.

Here is an example of how you could use the decorator:

class MyClass {
  @readonly
  myProp: string;
}

const myClass = new MyClass();
myClass.myProp = 'hello'; // This should throw an error
console.log(myClass.myProp);

Output:

Cannot assign to read only property 'myProp'

Solution:

function readonly(target: Object, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
  });
}

Create a decorator function called memoize that caches the results of a function and returns the cached value if the function is called with the same arguments.

Here is an example of how you could use the decorator:

class MyClass {
  @memoize()
  slowFunction(a: number, b: number) {
    console.log('This function is slow');
    return a + b;
  }
}

const myClass = new MyClass();
console.log(myClass.slowFunction(1, 2)); // This should log "This function is slow" and return 3
console.log(myClass.slowFunction(1, 2)); // This should not log anything and return 3
console.log(myClass.slowFunction(3, 4)); // This should log "This function is slow" and return 7

Output:

This function is slow
3
This function is slow
7

Solution:

function memoize() {
  return function(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cache = new Map<string, any>();
    descriptor.value = function(...args: any[]) {
      const key = JSON.stringify(args);
      if (!cache.has(key)) {
        cache.set(key, originalMethod.apply(this, args));
      }
      return cache.get(key);
    };
  };
}

Create a decorator function called throttle that limits the number of times a function can be called within a given time interval.

Here is an example of how you could use the decorator:

class MyClass {
  @throttle(500)
  myMethod() {
    console.log('This method is called');
  }
}

const myClass = new MyClass();
myClass.myMethod(); // This should log "This method is called"
myClass.myMethod(); // This should not log anything
setTimeout(() => {
  myClass.myMethod(); // This should log "This method is called"
}, 600);

Output:

This method is called
This method is called

Solution:

function throttle(interval: number) {
  return function(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    let lastCall = 0;
    descriptor.value = function(...args: any[]) {
      const currentTime = Date.now();
      if (currentTime - lastCall > interval) {
        lastCall = currentTime;
        return originalMethod.apply(this, args);
      }
    };
  };
}

Create a decorator factory function called logCalls that logs the arguments and return value of a function every time it is called.

Here is an example of how you could use the decorator:

class MyClass {
  @logCalls()
  myMethod(a: number, b: number) {
    return a + b;
  }
}

const myClass = new MyClass();
console.log(myClass.myMethod(1, 2)); // This should log "myMethod called with arguments 1, 2 and returned 3"
console.log(myClass.myMethod(3, 4)); // This should log "myMethod called with arguments 3, 4 and returned 7"

Output:

myMethod called with arguments 1, 2 and returned 3
myMethod called with arguments 3, 4 and returned 7

Solution:

function logCalls() {
  return function(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
      console.log(`${propertyKey} called with arguments ${args} and returned ${originalMethod.apply(this, args)}`);
    };
  };
}