DEV Community

Cover image for Decorate your code like a pro
Kinanee Samson
Kinanee Samson

Posted on • Updated on

Decorate your code like a pro

Typescript, a superset of JavaScript that gives developers god like control over their code, today we are going to be talking about one of the coolest features in TypeScript; Decorators. You might wonder what they hell are decorators and what they do? Decorators are an experimental feature of typescript and still remain subject to change. That being said, decorators are simply just a way to extend your class with meta data or annotate your code. Why on earth would you want to add some extra data of your own to your data? Well for sorting and grouping purposes. Decorators also allows us to achieve Behaviour Driven code. I think decorators are really cool, in most use cases for creating a decorator you will agree with me that it makes your life much easier to deal with and things are managed properly. It can also allow us to achieve good degree of polymorphism in the way we write our code. Ensure to add "experimentalDecorators": true in your ts.config file to enable decorators.

// HOOW DECORATORS APPEAR WHEN USED
@foo()
class Persoon {
    //...

    @Greet('Hey')
    greet () {
        //..
    }
}
Enter fullscreen mode Exit fullscreen mode

If you've used angular you will be pretty familiar with this and could even have a very deep understanding of how they work. To be more specific stuffs like @Input() and @Output() and some others we use regularly are decorators. Even if you are not too familiar with decorators do not panic, they are quite easy to understand and by the end of this article, you will be creating decorators for fun. Let's create our own class decorator. I will just keep the example bare to the minimals;

// SIMPLE DECORATOR FUNCTION
@logProto
class Hero {
    constructor(public name: string){}
}
// THE ACTUAL FUCNCTION
fucntion logProto(constructor: Function){
    console.log(constructor.prototype)
}
Enter fullscreen mode Exit fullscreen mode

What we created above is a simple class decorator, you will agree with me that it was really simple to create one right? Other than classes we can also decorate methods, properties, accessors and parameters. It is often more practical and useful to create a decorator factory rather just a decorator function. A class decorator will only accept one parameter which represents the the class itself, it is often called constructor.

Decorator Factories

Decorator factories simplify the creation of decorators by allowing us to pass in custom parameters to a decorator function. A decorator factory is simply a function that returns the actual decorator function. The parameters that decorator functions are expecting is quite rigid. We can't pass in any argument we like directly to the decorator function so we wrap that function inside another function that will return the decorator function. Let's augment our class decorator and add meta data to the class it is applied to.

// DECORATOR FACTORY
function addMetaData(data: string){
    return function(constructor: Function){
        constructor.prototype.type = data
    }
}

@addMetaData('Human')
class HumanHero {
    constructor(public name: string){}
}

console.log(HumanHero.prototype) // { type: "human"}
Enter fullscreen mode Exit fullscreen mode

One thing about class decorators is that if that class serves as a base class to other class, the sub classes will not inherit the actual decorator function, so it is often best to freeze or seal the object so that other classes don't inherit from it. we can call Object.freeze(constructor) or Object.seal(constructor) inside the decorator function to prevent that class from being inheritable.

function freezeObj(){
    return function(constructor: Function){
        Object.freeze(constructor)           // Or Object.seal(constructor)
        Object.freeze(constructor.prototype) // Or Object.seal(constructor.prototype)
    }
} 

@freezeObj()
class HumanHero {
    constructor(public name: string){}
}

class AlienHero extends HumanHero {
  // not possible, will throw an error 
}
Enter fullscreen mode Exit fullscreen mode

Method Decorators

Method decorators allow us to alter the behavior of methods defined in our classes, it is often useful to allow us achieve polymorphism. And we can even override the actual logic written inside the method with the decorator function, but that is not practical. Most often we want to give our methods some super heroic features. Method decorators accepts three parameters; a target which is the method's parent class, the key which represents the name of the method inside the class and a descriptor object that holds the original function logic, let's create a simple method decorator;

// METHOD DECORATORS
function logArgs(){
    return function (target: any, key: string, descriptor: PropertyDescriptor){
        console.log(`target -${target}, key - ${key}`, descriptor)
    }
}

class Hero {
    constructor(public name: string){}

    @logArgs()
    greet(){
        //..do something
    }
}

// IN YOUR CONSOLE
// target - Hero(), key - greet { enumerable: false, writeable: true, value: greet(), configurable: true}
Enter fullscreen mode Exit fullscreen mode

The above decorator we created does nothing, just wanted to show you what each argument actually is when we apply the decorator factory to a class method. Now let's write a decorator function that will allow a hero to smile before saying anything.

function express(mood: string){
    return function(target: any, key: string, descriptor: PropertyDescriptor) {
        const original = descriptor.value
        descriptor.value = function() {
            console.log(mood)
            const result = original.apply(this)
            return result
        }
        return descriptor
    }
}


class Hero {
    constructor(public name: string){}

    @express('smiles')
    greet(){
        console.log(`${this.name} says hello`)
    }
}

const supes = new Hero('superman')
supes.greet() // smiles, superman says hello
Enter fullscreen mode Exit fullscreen mode

Property Decorators

Property decorators are used on the properties of a class, they can allow us to do some custom logic, they are one of the simplest decorators to implement and they only require two parameters, the target which is the class the property belongs to and the key which represents the name of the property.

function decorateKey(customValue: string){
    return function(target: any, key: string){
        target[key] = customValue
    }
}


class Hero {
    constructor(public name: string){}

    @decorateKey('dcu')
    public world
}

const superman = new Hero('superman')

console.log(superman.world) // dcu
Enter fullscreen mode Exit fullscreen mode

Accessors Decorators

Accessors decorators are similar to function decorators, they even accept the same arguments as function decorators, they allow us to extend or modify the behavior of accessors. One thing we cannot do is pass arguments to the get method of any class, but we can create a decorator function that allows us to that.

function decorateAccessor (val: string){
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value

    descriptor.value = function(){
      const res = original.apply(this)
      return `${res} ${val}`
    }

    return descriptor
  }
}

class Hero{
  constructor(public name: string){}

  private _alias: string

  set(val: string) {
    this._alias = val
  }

  @decorateAccessor('yeeps')
  get(){
    return this._alias
  }
}

const superman = new Hero("superman")
superman.set('clark Kent')

console.log(superman.get()) // yeeps clark Kent
Enter fullscreen mode Exit fullscreen mode

You would agree with me that decorators are really simple after going through this, i hope you enjoyed this and learned something from it, thanks until next time.

Top comments (0)