We are currently developing a Web services framework in TypeScript that runs in the Deno environment and uses decorators heavily.

This is a short piece of framework test code.

import { Controller } from ".. /.. /Server.ts";
import { Get, Post, Query } from ".. /.. /Router.ts";

interface User {
  id: string;
 name: string; }  let users: User[] = [  {  id: "1". name: "Zhang". },  {  id: "2". name: "Xiao li". }, ];  @Controller("user") export class UserController {  @Get("find")  findById(@Query("id") id: string): User | undefined {  return users.find((user) = > user.id === id);  } } Copy the code

Used Java friends will feel very familiar, a very strong Spring wind is not it?

Those of you who have used NodeJS will probably smell Nestjs as well.

In fact, the framework I’m currently working on was inspired by Spring. I’m not really inspired, I just like this style of server-side code.

Before that, I used a lot of server-side frameworks, such as Java’s Spring series, Django for Python, Go’s Gin, NodeJS express, KOA, NestJS, NextJS, and many more. But I still feel that Spring is the best designed and easiest to use in enterprise-level server development.

This feeling is not a sudden emotion or because I am using Spring. In fact, I haven’t written much Java code in the past two years, and I haven’t even written much Web server code.

The style of the above code is quite different from that of a JavaScript web framework.

Analogies to other JavaScript Web frameworks, such as KOA, which I think is great, code in this style.

const router = require("koa-router") ();
router.get("/xx".async (ctx, next) => {
  ctx.body = "hello";
});
 module.exports = router; Copy the code

This style is also good.

The biggest difference between the two styles of code is actually the difference between the object-oriented style and the functional style, regardless of the functionality implemented.

Koa also has TypeScript versions that support the above object-oriented style of code, though few people use it.

If you use TypeScript, you’re pretty sure that your code style is going to be largely object-oriented. Very few people write functional style code in TypeScript. JavaScript is probably better than TypeScript.

Last year I spent some time writing a component library in Ramda and TypeScript, but the coding experience was awful.

In general, people who are interested in functional expressions are very dualistic and write very concise code. But it’s easy to be extreme, even if simplicity trumps everything, including performance and readability. TypeScript is characterized by stability and, by contrast, cumbersome code. Of course you can write short code by configuring to turn off all ts detection, but how is that different from using JavaScript directly?

So I don’t think TypeScript is suitable for writing functional style code.

All right, back to the point.

Can JavaScript write decorators?

The answer is not yet.

The decorator proposal was put forward on April 30, 2017, but after three long years, it is still in the second stage.

proposal-decorators

I don’t know when the bill will be officially approved. In fact, I think this way of language development can extend the life of JavaScript to some extent.

Although not supported by official standards, many open source projects have been using this feature through translators for a long time. All typescript-based projects support this feature.

TypeScript does not support this writing by default, so set “experimentalDecorators” to “experimentalDecorators” in tsconfig.json

True.

The essence of a decorator is a higher-order function that comes from the decorator pattern. It’s found in many languages.

Decorators are a JavaScript concept, so they are triggered in JavaScript precompilation, not TypeScript compilation.

Parameter differentiation

If you subdivide decorators at the level of parameter form, there are two types.

One with no parameters, one with parameters.

A Decorator Decorator

A standard decorator is one that has no parameters.

In the following example, @meWing can hijack the Cat class, pass the Cat constructor into mewing, and then perform some operations in mewing.

@mewing
class Cat {
  constructor() {
    console.log("Meow!");
  }
}  function mewing(target: any) {  console.log("Meow!"); } Copy the code

Running the above code with NodeJS prints “Meow!” on the console. , even if the new operation is not performed.

As explained above, the decorator logic is executed during JavaScript precompilation.

Because the decorator is executed during the precompilation phase, it cannot interfere with subsequent instances created. But you can create instances of classes in decorators, but that doesn’t make sense.

Trimmer with reference: Trimmer factory

Decorators with parameters can pass metadata to decorators, such as controlling the number of meows.

@mewing(4)
class Cat {
  constructor() {
    console.log("Meow!");
  }
}  function mewing(num: number) {  return (target: any) = > {  for (let i = 0; i < num; i++) {  console.log("Meow!");  }  }; } Copy the code

You’ll hear four meows.

The only difference between a parameterized decorator and a no-parameter decorator is that a parameterized decorator wraps a function around it. The outer function is the decorator factory function, and the inner function is the actual decorator.

Classification of types

On the level of type, decoration is divided into 5 types.

They are ClassDecorator, PropertyDecorator, AccessorDecorator, MethodDecorator, and ParameterDeco Rator).

Here are the five types of declarations, where the accessor decorator and the PropertyDecorator share the PropertyDecorator type.

declare type ClassDecorator = <TFunction extends Function> (  target: TFunction
) => TFunction | void;
declare type PropertyDecorator = (
  target: Object. propertyKey: string | symbol ) = >void; declare type MethodDecorator = <T>(  target: Object. propertyKey: string | symbol,  descriptor: TypedPropertyDescriptor<T> ) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (  target: Object. propertyKey: string | symbol,  parameterIndex: number ) = >void; Copy the code

Class declaration decorator ClassDecorator

The class declaration decorator can modify the definition of a class directly.

It takes an argument, which is the constructor of the original class, and optionally returns a class. If a class is returned, the original class can be replaced.

@mewing(2)
class Cat {
  constructor() {
    console.log("Meow!");
  }
}  function mewing(num: number) {  return (target: any) = > {  return class Dog {  constructor() {  for (let i = 0; i < num; i++) console.log("Woof!");  }  };  }; }  new Cat(); Copy the code

The code above runs as “Woof!” Instead of “Meow! That is, ostensibly a cute kitten who has become a dog.

Note how many methods and attributes the original class had, and the new returned replacement class will need to implement and create the same number of methods and attributes.

You can also not return any value, but only modify its prototype, such as extending its methods.

@mewing(0)
class Cat {
  constructor() {
    console.log("Meow!");
  }
}  function mewing(num: number) {  return (target: any) = > {  target.prototype.cute = (a)= > {  console.log("🐱");  };  }; }  new Cat().cute(); Copy the code

Note that you cannot modify target directly in the class decorator, because the class definition is already complete and you can only replace the original definition or extend the prototype here.

MethodDecorator

Method decorators can only be applied to class methods.

Unlike the class decorator, it takes an extra descriptor argument. This parameter is valid when the build target is greater than or equal to an ES5 release.

If the build target is an ES3 version, this parameter is undefined. Virtually no one is building es3 versions of JavaScript code anymore.

What descriptor does is set the description of that property, you can either change the property directly, or return a new object instead of the default description.

let defaultDescriptor = {
  value: [Function].  writable: true.  enumerable: true.  configurable: true.}; Copy the code

Here is a Demo from the official Demo.

class Cat {
  constructor() {
    console.log("Meow!");
  }
  @enumerable(true)
 cute() {  console.log("🐱");  } }  function enumerable(value: boolean) {  return function (  target: any. propertyKey: string. descriptor: TypedPropertyDescriptor<any>  ) {  descriptor.enumerable = value;  }; }  for (let i in new Cat()) {  console.log(i); } Copy the code

You’ll find that the cute method becomes an unenumerable state.

Method decorators can modify corresponding methods or add new methods. Such as:

class Cat {
  constructor() {
    console.log("Meow!");
  }
  @wawa
 cute() {  console.log("🐱");  } }  function wawa(  target: any. propertyKey: string. descriptor: TypedPropertyDescriptor<any> ) {  descriptor.value = (a)= > console.log("wawa");  target.dudu = (a)= > console.log("dudu"); }  new Cat().cute(); new Cat().dudu(); Copy the code

PropertyDecorator & AccessorDecorators

Property decorators are very similar to accessor decorators, so we’ll talk about them together.

The property decorator is actually the constructor member decorator of the class.

For example, you can set default values.

class Cat {
  @rename name: string | undefined;
}

function rename(target: any, propertyKey: string) {
 target[propertyKey] = "Flower"; }  console.log(new Cat().name); Copy the code

Of course, you can do something even weirder, such as setting property descriptions for this property and accessors.

class Cat {
  private _age: number = 1;
  @initName name: string | undefined;
  @watch
  get age() {
 return this._age;  }  set age(v: number) {  this._age = v;  } }  function initName(target: any, propertyKey: string) {  target[propertyKey] = "My name is Dumb."; }  function watch(  target: any. propertyKey: string. descriptor: PropertyDescriptor ) {  let { set.get } = descriptor;  descriptor.set = function (v: number) {  console.log('You're setting a new one${propertyKey}, the new value is:${v}. `);  set? .call(target, v); };  descriptor.get = get? .bind(target);}  let cat = new Cat(); cat.age = 2; console.log(`name: ${cat.name} age: ${cat.age}`); Copy the code

Accessor decorators actually work much like attribute decorators.

But because decorators can’t access instance objects, the visitor decorator is a bit of a chicken.

If you change one of the properties on descriptor, then all the other properties have to be reset. If you still want to use the properties set in the original class, you need to bind this to target. The target is the constructor of the class, but after the decorator, everything becomes new. If you set more than one visitor decorator, then you share the same target and descriptor in multiple visitor decorators.

For example, add an @watch2 decorator and move the get logic to @Watch2. After the precompilation, the set set in @watch is not overwritten.

class Cat {
  private _age: number = 1;
  @initName name: string | undefined;
  @watch
  @watch2
 get age() {  return this._age;  }  set age(v: number) {  this._age = v;  } }  function initName(target: any, propertyKey: string) {  target[propertyKey] = "My name is Dumb."; }  function watch(  target: any. propertyKey: string. descriptor: PropertyDescriptor ) {  let { set } = descriptor;  descriptor.set = function (v: number) {  console.log('You're setting a new one${propertyKey}, the new value is:${v}. `);  set? .call(target, v); }; }  function watch2(  target: any. propertyKey: string. descriptor: PropertyDescriptor ) {  let { get } = descriptor;  descriptor.get = get? .bind(target);}  let cat = new Cat(); cat.age = 2; console.log(`name: ${cat.name} age: ${cat.age}`); Copy the code

ParameterDecorator is a ParameterDecorator

The parameter decorator takes three parameters: the class’s construction, the method name, and the parameter’s subscript.

class Cat {
  constructor() {
    console.log("Meow!");
  }
  cute(@emoji emoji: string) {
 console.log(emoji);  } }  function emoji(  target: any. propertyKey: string | symbol,  parameterIndex: number ) {  let params = [];  params[parameterIndex] = "🐈"; target[propertyKey] = target[propertyKey].bind(target, ... params);}  new Cat().cute(); Copy the code

The parameter decorator cannot change the definition of the function to which it belongs, so the above code is not expected.

But it can add other methods, such as the following:

class Cat {
  constructor() {
    console.log("Meow!");
  }
  cute(@emoji emoji: string) {
 console.log(emoji);  } }  function emoji(  target: any. propertyKey: string | symbol,  parameterIndex: number ) {  let params = [];  params[parameterIndex] = "🐈"; target.cute2 = target[propertyKey].bind(target, ... params);}  new Cat().cute2(); Copy the code

There is, of course, a way to change the definition of the method to which it belongs, but it is very cumbersome. The corresponding data can be attached to the target and retrieved from the target through the method decorator, where the method can be redefined.

Decorator execution order

Execution order when there are multiple decorators in the same location

Called as a pipe, each decorator factory method is called from the top down, and decorator methods are called from the inside out. The whole process is analogous to the Onion model.

@a
@b
c
Copy the code

It’s going to look something like this.

a(b(c));
Copy the code

Parameter decorator execution order

Execution starts with the last parameter.

class Cat {
  constructor() {
    console.log("Meow!");
  }
  cute(@emoji emoji: string.@voice voice: string) {
 console.log(emoji, voice);  } }  function emoji(  target: any. propertyKey: string | symbol,  parameterIndex: number ) {  console.log("emoji call"); } function voice(  target: any. propertyKey: string | symbol,  parameterIndex: number ) {  console.log("voice call"); }  // voice call // emoji call Copy the code

Different types of decorators are executed in order

Except for class and parameter decorators, all decorators are executed in the order in which they appear.

In a method decorator, if a parameter decorator is encountered, it is executed in the same order as the parameter decorator.

The class decorator is always executed last.

Here’s 50 lines of code to figure out the order.

@clazz(a)class CallSequence {
  @property() property: undefined;
  @method(a)  mtehod(@parameter1() p: any.@parameter2() p2: any) {}
 private _a = 1;  @accessor(a) get a() {  return this._a;  } }  function clazz() {  console.log("ClassDecorator before");  return (target: any) = > {  console.log("ClassDecorator after");  }; }  function method() {  console.log("MethodDecorator before");  return (t: any, k: any, p: any) = > {  console.log("MethodDecorator after");  }; }  function property() {  console.log("PropertyDecorator before");  return (t: any, k: any) = > {  console.log("PropertyDecorator after");  }; }  function accessor() {  console.log("AccessorDecorators before");  return (t: any, k: any) = > {  console.log("AccessorDecorators after");  }; }  function parameter1() {  console.log("ParameterDecorator1 before");  return (t: any, k: any, i: any) = > {  console.log("ParameterDecorator1 after");  }; }  function parameter2() {  console.log("ParameterDecorator2 before");  return (t: any, k: any, i: any) = > {  console.log("ParameterDecorator2 after");  }; } Copy the code

Print result:

PropertyDecorator before
PropertyDecorator after
MethodDecorator before
ParameterDecorator1 before
ParameterDecorator2 before
ParameterDecorator2 after
ParameterDecorator1 after
MethodDecorator after
AccessorDecorators before
AccessorDecorators after
ClassDecorator before
ClassDecorator after
Copy the code

use

The purpose of decorators is to make metaprogramming easier. For metaprogramming, there is another library, Reflect-Metadata, which was born on March 12, 2015, more than five years ago, yet has not even been proposed. Because it needs to wait for the decorator proposal to be approved before it can be proposed.

However, thanks to the power of current conversion compilers, you can use them if you want.

You can use them for anything you want, such as printing logs, parameter injection, etc., or as frameworks.

Because decorators don’t have access to instances of classes, it sometimes feels like a handicap, which is not what decorators are designed for.

The original intention of the decorator is to change the definition of a class before it is created.

Other than that, it is not the original purpose of the decorator, but the decorator does not limit.

To what extent it can be applied depends on the individual.

The features, concepts, and usages described above only apply to ts39 in its current Stage (Stage 2, 09/06/2020) and are subject to change.

First published in JavaScript Jikesha Academy