Quantcast
Channel: Stories by Uday Hiwarale on Medium
Viewing all articles
Browse latest Browse all 145

Anatomy of TypeScript “Decorators” and their usage patterns

$
0
0

TypeScript: New Features

In this lesson, we are going to learn about the Decorator pattern in TypeScript and how Decorators are used to change the behavior of the classes. We are also going to see how the reflect-metadata package helps us to design decorators with ease.

(source: unsplash.com)

Decorators are annotations that you put on the top of a class declaration or a class member and it changes how that class or the field behaves. If you are an Angular developer, then you perhaps know about the @Component decorator which defines an Angular component.

(source: angular.io)

In the above example, the @Component annotation is a decorator that decorates the AppComponent class. It basically turns this class into an Angular component with the configuration provided with the decorator annotation. Similarly, the @Input annotation on a class instance field is a decorator.

We talked about metaprogramming and its use-cases in JavaScript in earlier lessons. Metaprogramming, in a nutshell, is a programming pattern to introspect and control the behavior of programs. For example, the @Component decorator above changes the behavior of the AppComponent class.

Decorator is nothing but a JavaScript function, but when it is placed on the top of a class or its members with the @ prefix, it gets invoked at the runtime with certain arguments. These arguments represent the internals of the class or member it is decorating. Within this function, we can change these internals which changes the behavior of the program.

Decorators are not a TypeScript feature, unlike enums or interfaces. It is a native JavaScript feature but it has to be yet standardized. This proposal is stage-2 of the ECMAScript proposal track. However, we can implement decorators in JavaScript using a Babel plugin. We have explored decorators in great detail in the “A minimal guide to JavaScript Decorators” lesson where I have also shown you how to transpile decorators using Babel CLI.

Since decorators are not part of the ECMAScript proposal at the moment and it is in the stage-2 of the ECMAScript proposal track, it is under active development. It has undergone significant changes recently which is why I needed to revise the above article on decorators (older version here).

TypeScript has implemented the decorators when the proposal was in its early stage which means what proposal specifies (at the moment) and what TypeScript implements no longer matches. Even though TypeScript implements what we now call the legacy version of the decorator proposal, decorators are quite interesting and very useful.

💡 TypeScript doesn’t seem eager at the moment to implement the changes in the decorator proposal. Since these changes would introduce breaking changes in the application and god knows how many third-party packages would be affected, I guess it is in the best interest of everybody to wait until the proposal is stable or ready for the inclusion in the ECMAScript standard.

Since decorators are not part of the ECMAScript standard and they are considered experimental at the moment, TypeScript won’t let you use decorators without explicitly taking the responsibility. Therefore, you need to set experimentalDecorator compiler-option to true in the tsconfig.json file or provide --experimentalDecorator compiler-flag in the command.

{
"files": [
"program.ts"
],
"compilerOptions": {
"target": "ES6",
"experimentalDecorators": true,
"removeComments": true,
"alwaysStrict": true
}
}
💡 In the above tsconfig.json file, I have set the target to ES6 just so that it would be easier to see how decorators are operating in the compiled program, but you are free to choose lower targets as well. TypeScript will down-compile decorators and classes according to the target you choose.

Decorators can only decorate classes or its members (such as properties, methods, accessors, etc). TypeScript supports decorators for class declarations, methods, accessors (getter/setter methods), method parameters (including constructor’s), and class properties.

Class Declaration Decorator

The @Component decorator we saw earlier is a type of class declaration decorator or simply class decorator as it modifies the class itself. A decorator is a simple JavaScript function and nothing else. So let’s create a simple decorator that freezes the class and its prototype.

(decorators/class-decorator.ts)

In the above program, we have declared the class Person which is a simple JavaScript (ES6) class with some static properties, some instance properties, and some instance methods, so nothing special here. However, we have decorated this class with the @lock decorator.

As the decorator goes, lock is a JavaScript function that will be invoked by the JavaScript engine at runtime. When the lock function is executed, it is invoked with the class’s constructor function as the only argument as this decorator is decorating the class.

This argument confuses some people since we are decorating a class and the argument received is the constructor function. What’s up with that? Well, class is a fancy keyword we received in ES6 to create classes. But under the hood (in JavaScript engine), it is broken down to a constructor function and prototype. The above program is analogous to the below program.

function Person(fname, lname) {
this.fname = fname;
this.lname = lname;
}
Person.version = 'v1.0.0';
Person.prototype.getFullName = function() {
return this.fname + ' ' + this.lname;
}

So the constructor function we are talking about is the Person function in the above example. This is what we receive in the decorator function as the only argument. It also has the prototype as the static property with it. So whenever somebody says a class, imagine a constructor function with the body of the class.constructor method and its prototype having all the instance methods of that class.

In the lock decorator, using Object.freeze() method, we froze both the constructor function and its prototype, therefore we can’t add or modify any properties on them at runtime which is evident from the errors shown in the comments.

You must be wondering, if @ prefix is not supported in JavaScript, how the heck this is still working. Well, when you compile the program, the TypeScript compiler removes the @ prefix along with the decorator name and replaces it with a helper function call that executes the decorator function.

The above program is the compiled code of the class-decorator.ts file that was compiled using tsc command. Here, the __decorate is the helper function that calls the lock decorator function with the Person class.

At the moment, our lock decorator function is not returning any value. If a class decorator returns a value, then it must be a constructor function (or a class) that will replace the original constructor function (or a class). Here, you would need to take the responsibility to maintain the original prototype.

function decorator(class) {
// modify 'class' or return a new one
}
(decorators/class-decorator-extend.ts)

In the above example, the withInfo decorator function is generic. The T type parameter represents the static side type of the class which is equivalent to :typeof Person annotation. TypeScript implicitly provides this type of the class to the decorator function it is decorating. The T extends Ctor generic constraints only validate if the incoming type is a class so that this decorator can only be used on the classes and not on its internal members.

💡 We have discussed Generic Types and Generic Constraints in this lesson. If you want to know more about the static side type of a class, follow this lesson.

From within the withInfo decorator function, we are returning a new class that extends the Person class. This new class doesn’t have a constructor which means the Person constructor will get called implicitly. The person object and its prototype chain looks like below.

(Chrome DevTools)

Method Decorator

A method decorator decorates the static or instance method of the class (except the constructor). In this case, the decorator function receives three arguments. The first argument is the target to which the method belongs. It could be the constructor function (class) if the method is static or the prototype of the class if the method is an instance method.

function decorator(target, name, descriptor) {
// modify 'descriptor' or return new one
}

The second argument is the name of the method and the third argument is the property descriptor of that method. It’s not necessary to return a value from this decorator function but if you do, then it must be a new property descriptor of the method which will replace the old one.

(decorators/method-decorator.ts)

In the above example, readonly decorator function only modifies the writable setting of the property descriptor but the prefix decorator function returns a new property descriptor. The value field of a property descriptor of a method contains the method implementation (function).

💡 When the compilation target is set to ES3, the method decorator function won’t receive the third argument which is the property descriptor. Also, the return value (property descriptor) is also ignored. This is due to poor support of the property descriptors in ES3.

Accessor Decorators

There is virtually no difference between an accessor decorator and a method decorator. When a static or instance method has get or set prefix, we call them accessors. When a method has get prefix (getter method), its property descriptor has the get field which contains the method definition instead of value field. Similarly, the setter method is stored in the set field.

class Person {
constructor(
public fname: string, public lname: string
) {}
    get fullname(): string {
return this.fname + ' ' + this.lname;
}
    set fullname( name ) {
[ this.fname, this.lname ] = name.split(' ');
}
}

Getter and setter methods with the same name do not have independent property descriptors. For example, fullname method in the above example is technically a single property whose property descriptor has get and set fields containing these function implementations.

Therefore, though it would seem fair to provide the same or different decorators for these accessors individually, but that practice is discouraged in TypeScript. You can decorate both accessors using a single decorator which should be added to the first accessor such as getter in the below example.

(decorators/accessor-decorator.ts)

In the above example, using the uppercase decorator function, we have returned a new property descriptor for the fullname accessor methods. This property descriptor contains the get and set fields.

Property Decorators

We can also decorate class static and instance properties. The decorator function for these properties receives only two arguments. The first argument is the target which could be the constructor function if the property is static or the class prototype if the property is an instance property. The second argument is the name of the property.

function decorator(target, name) {
// collect or store some information
}

This decorator is a little unusual. An instance property (AKA field) is generated on the instance when that instance is created. Hence, we can’t really configure the property descriptor of an instance property on the class. TypeScript doesn’t have a mechanism in place to decorate a class field but the decorator proposal has a solution for it which we saw in the JavaScript Decorators lesson.

Therefore a property decorator function does not get invoked with the property descriptor in TypeScript. Also, the return value of the decorator function is ignored. So why we would need a property descriptor? Well, it could be used to capture the information about the property and later use it.

Introduction to “reflect-metadata” package and its ECMAScript proposal

In the above lesson, I talked about the Reflect Metadata proposal and where it can be used. The reflect-metadata package is a polyfill for this proposal. I would recommend you to read this lesson before moving forward else you won’t be able to understand a thing. So please read it first.

The Reflect.defineMetadata(key, value, target, prop) method defines a metadata value value with a unique key key for the target object or for the prop property of the target. Using the Reflect.getMetadata method, you can extract this metadata later.

(decorators/property-decorator.ts)

In the above example, the textCase decorator function sets a metadata value using Reflect.defineMetadata method for the decorator’s target and property name with the case key. This metadata value is a simple string which tells about the formatting case for that property. So the fname should be transformed to uppercase and lname to lowercase.

Later, we extract this metadata in the fullname getter method using the Reflect.getMetdata method. Since this inside an instance method (or accessor) points to the instance, this isn’t the same as the target we used in the decorator function (which was Person.prototype) to store metadata.

But since the Reflect.getMetadata(key, target, prop) method also searches on the prototype of the target argument, which would be the prototype of this which is the Person.prototype, it was able to find the metadata value. It wouldn’t be the case with Reflect.getOwnMetadata() method though.

@Reflect.metadata(metadataKey, metadataValue)

The Reflect.metadata() returns a decorator function. Therefore you don’t need to write a decorator function yourself like the textCase decorator function we wrote above.

The decorator function returned by this method works for everything, so you can use it to decorate classes, methods, accessors, etc. This decorator is used to store some metadata for the given target or a property of the target. Internally, the returned decorator function calls the Reflect.defineMetada method with the metadataKey, metadataValue and target and/or property.

(decorators/property-decorator-reflect.ts)

The Reflect.metadata method works just like the textCase decorator function. The Reflect.metadata accepts a metadata key and a metadata value returns a decorator function that applies metadata on the target (when class) or on the property (other) it is decorating.

Parameter Decorator

A parameter decorator decorates method parameters including the parameters of the constructor. The decorator function for these properties receives three arguments. The first one is the target which could be the constructor function if the method is static or the class prototype if the method is an instance method.

The second argument is the name of the method while the third argument is the ordinal index of the parameter in the method definition.

function decorator(target, name, index) {
// collect or store some information
}

Since we are talking about the parameters and not properties, there is no question of receiving or returning property descriptors. Like parameter decorators, these decorators are also used to collect or store some metadata regarding the parameters.

(decorators/parameter-decorator.ts)

In the above example, the textCase decorator function decorates parameters of the constructor. The method name name for the constructor function comes as undefined in the decorator function and the target comes as the class itself since it belongs to the class rather than the prototype.

Using the index argument, we are storing a metadata value for each parameter. Since the metadata value is identified with the index of the parameter, we have used 0 and 1 values along with undefined as the method name to extract that metadata in the fullname getter method.

Getting better at Decorators

Decorator factory

A decorator factory is a function that returns a decorator. So technically the Reflect.metadata method we used earlier is a decorator factory as it returns an actual decorator function. When you want to decorate something with a decorator factory, we need to call that function in the decorator annotation syntax such as @decoFactory(...args).

(decorators/class-decorator-factory.ts)

As you can see in the above example, the version function is a decorator factory. Therefore, it must be annotated with the @version(...) call signature while decorating a class.

A decorator factory is useful to customize a decorator before applying it to something. You will see decorator factory used heavily than the decorator functions since it provides customization features and it can be reused.

Decorator chaining

You can chain multiple decorators together. There are two ways to chain two ore more decorators on a single entity. You can put them on top of each other as shown below. In this case, the decorators will be evaluated from top to bottom but applied from bottom to top.

@decoratorA
@decoratorB
entity

You can also put them side to side as shown below. In this case, the decorators are evaluated from left to right but applied from right to left.

@decoratorA @decoratorB entity

In both cases, the decoratorA is evaluated first so that if the decoratorA is a decorator factory, the decorator function is collected. Once all decorators are evaluated, they are applied in reverse order. Therefore, the decoratorB is applied first and then the decoratorA. Let’s see this in action.

(decorators/decorator-chaining.ts)

As you can see from the above results, the factory functions were called in the order decorators were chained but the actual decorators were applied in the reverse order. You can also mix the decorator factories with decorator functions as did in the name parameter decoration.

Decoration Order

In the above result, you can clearly see that the class decorators were evaluated before the parameter decorators however the parameter decorators were applied first. Let’s see how different kinds of decorators are evaluated and applied with respect to each other.

(decorators/decoration-order.ts)

In the above example, the factory decorator factory is the same as the decoratorFactory used in the previous example. In this example, we have decorated almost everything we can decorate. From the results, we can see the order of decoration.

  1. First, the instance properties are decorated followed by the instance method parameters, instance methods, and lastly the instance accessors.
  2. Then static properties are decorated followed by the static method parameters, static methods, and lastly the static accessors.
  3. Then constructor parameters decorated.
  4. In the end, the class is decorated.

Emit Decorator Metadata

TypeScript provides the emitDecoratorMetadata compiler-option (or the --emitDecoratorMetadata compiler-flag) to implicitly add specific metadata to class or its members. When this option is set to true, the TypeScript compiler injects some extra code in the compiled JavaScript while using decorators.

{
"files": [
"program.ts"
],
"compilerOptions": {
"target": "ES6",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"removeComments": true,
"alwaysStrict": true
}
}
(decorators/emit-decorator-metadata.ts)

In the above program, we have created a simple decorator function noop that does nothing. Then we decorated the Person class and getNameWithPrefix method with this decorator. Inside getNameWithPrefix method, we extracted some metadata using the Reflect.getMetadata method and some weird looking keys. When and where did we set this metadata?

To understand where this metadata has been set, we need to see the compiled JavaScript code because the logic to add this metadata is added during the compilation by the TypeScript compiler.

As you can see in the above code snippet, the TypeScript compiler automatically applied few more decorators (along with noop) to the getNameWithPrefix method as well as to the Person class using the __metadata help function which is nothing but a decorator factory.

This decorator factory returns a decorator that stores metadata for the given target or its property with the help of Reflect.metdata method. These decorators will only be applied to the class or class members with decorator annotations. The role of these decorators are as follows.

  • This decorator stores the runtime data type of the entity it is decorating with the design:type key.
  • This decorator stores the runtime data type of the arguments of the method it is decorating with the design:paramtypes key.
  • This decorator stores the runtime data type of the return value of the method it is decorating with the design:returntype key.

Here the runtime data type is the value of the metadata. This data type is serialized from the TypeScript type. For example, string is serialized into String and number is serialized into Number. You can follow this list to know more about the serialization of other data types.

Things to watch out

As we learned, decorators are used to transforming the behavior of the class or its members. The way TypeScript achieves this is by using the helper functions that modify the class at runtime.

If a class is declared as ambient (using declare keyword) or inside a type declaration file, then this won’t be valid since not only it doesn’t make sense but ambient declarations or type declarations do not produce any output.

Also, you might have noticed that we have used Person as any type assertions in some of the programs. For example, if you take the class decorator factory example, the version decorator adds version static property on the Person class. But if you access version property on the Person, the TypeScript compiler would complain.

Property 'version' does not exist on type 'typeof Person'.
console.log( 'version ->', Person.version );
~~~~~~~

This happens because the version property is added to the Person dynamically at runtime and the TypeScript compiler can’t guess that during compilation. This issue is tracked here.

(thatisuday.com / GitHub / Twitter/ StackOverflow / Instagram)

Anatomy of TypeScript “Decorators” and their usage patterns was originally published in JsPoint on Medium, where people are continuing the conversation by highlighting and responding to this story.


Viewing all articles
Browse latest Browse all 145

Latest Images

Trending Articles



Latest Images