Decorators have been one of the most anticipated proposals in JavaScript for the past seven years, which also makes them one of the longest running active proposals to date.

They started back in 2015 and were iterated upon in tandem with classes and class fields with more and more features and capabilities being added as they advanced from stage one to stage two.

By 2019, the proposal was ready to move forward to stage three, but it had become so unwieldy and complicated that the committee decided it would not be possible citing concerns that it would make optimizing crucial flows in JavaScript classes, extremely difficult.

So with that, we were back to the drawing board.

We started from ground zero and completely rethought the proposal, building back up around the new constraints that the committee laid out, while also consulting library and framework authors and the wider JavaScript community as a whole.

It took a while, but after a lot of hard work and careful design, we were able to craft a proposal that satisfied the performance constraints while also maintaining the core features and flexibility of the original design.

And just this last March, the proposal finally made it to stage three, signaling that it had finally solidified and is nearing completion.

Today.

I want to take you through this new iteration of decorators to show you how it works and how you'll be able to use it in your applications soon.

Or now if you use transpilers like Babble.

I'll also cover the major differences between this proposal and its prior iterations, as decorators have had an unusually high adoption rate prior to stage three with many JavaScript users and frameworks that have been using them for years.

However, before we get into all of that, I wanna take a step back and take a look at what decorators are in the first place, how they work conceptually and why they deserve their own special syntax in the language.

Decoration is a meta programming pattern where a programmer abstracts some common functionality in a way that can be applied repeatedly to many different values.

Importantly, however, this usually doesn't change the fundamental type or shape of the value.

It just adds some extra behavior on top, thus 'decorating' it.

Decoration doesn't necessarily require its own syntax.

In fact, we can implement the decoration pattern in JavaScript for plain functions quite easily without one.

And it's a common pattern you may have seen before.

Let's take a look at this example, for instance.

Here we have a very simple add and subtract function.

And both of these functions have some common behavior that we may want to abstract out, specifically logging some debug information before and after the function has been called.

This functionality has three key characteristics that make it perfect for the decoration pattern.

First it is something that we may want to apply in many places-you could imagine adding this frequently to many functions in a code base to make debugging easier, although you would likely want to add a debug log level of some sort.

So you don't get a constant stream of log statements.

Second, the way you add it to a function is pretty much identical.

No matter what the function actually does.

In this case, our functions are pretty simple, but the same functionality can be added to any function in a very generic way.

And finally, it does not have a substantial impact on the function itself.

Adding this functionality doesn't completely change the way the function works.

In fact, in this case, it doesn't change it at all.

So let's see what this would look like if we were to apply the decoration pattern to it instead.

as you can see, we've abstracted out the logging functionality into this debug log function, which receives a function as its first parameter and returns a new function that calls the original and adds the logging in a generic way.

The implementation itself is a little bit more complicated, but we've dramatically reduced the amount of duplicated code and we've separated out concerns and importantly made it much easier for users of the debug log functionality to reason about and apply it to their code correctly.

Previously users would have to correctly add two log statements in each function and stash and return the result in between.

Now they can simply pass their function to debugLog, which allows them to focus on their own code and not the logging functionality.

While this pattern is easy enough to apply to functions, though, where it gets tricky is when we want to apply it to other types of values, especially classes.

It's possible to pass a class to a function and return a new class, but the result is not particularly easy to read, especially when you begin applying multiple levels of decoration.

More importantly, there simply is no way to apply decoration to other types of class values.

It's not possible to intercept the definition of a method or a field on a class and add functionality in a declarative, easy to read way.

And ultimately this has led to it being very difficult to meta program with classes in JavaScript.

This is in large part why this proposal is centered specifically around classes in order to add a syntax for us to be able to ergonomically apply the decoration pattern to methods, fields, and class definitions as, as a whole.

In the future, decorators may eventually work with plain functions and other types of values.

But the greatest need for a new syntax was here.

And that's why we've continued to pursue the feature after all this time.

Now that we have an understanding of why decorators are needed in the language, let's dig into the proposal as it stands today.

In this proposal, decorators are functions which receive a value of a certain type and can return another value of the same type.

So class decorators receive and return a class, method decorators receive and return a method.

And so on.

Decorator functions are applied to a decoratable value by using an @ sign, followed by the function name as a separate token in front of the value.

Decorators can be applied to classes, methods, getters and setters, fields, and a new type of class element, the class auto accessor, which we'll discuss a little later on.

Multiple decorators can be applied to the same value and are applied in order from the innermost decorator to the outermost decorator.

Decorator functions receive two parameters.

The first parameter is the value being decorated.

And the second is a context object, which contains a number of contextual values that assist with the decoration.

Moving on to some examples.

Here we have a class decorator and as we can see the value that is passed to the class decorator as the first parameter is the class definition itself.

This can be extended directly to create a new class or manipulated in other ways, for instance, by wrapping the class in a proxy.

The return value is then replaced for the value of the class itself.

In the case of a method, the value that is passed to the decorator is the function that is defined by the method on the class prototype.

This function can be wrapped with another function to add additional behavior.

But a key thing to remember here is that you cannot generally use an arrow function.

This is because most methods use `this` when they're on a class and the `this `context is important.

So we must preserve it by using the function keyword and function.apply.

In this example, we do that with a decorator that logs the results of a method whenever that method is called.

That method is then returned and defined on the prototype in place of the original.

Standard getters and setters work pretty much the same way.

The decorator receives the getter or setter function as the first parameter and returns a replacement.

This allows these decorators to pretty much be interchangeable with method decorators.

Class fields on the other hand are a little different.

Decoration happens just once when the class is first defined.

By contrast, class fields are defined on each instance of the class.

Decorators don't have a value to receive when they are being run.

So instead they return an initializer function, which is run every time the class is instantiated.

This function receives the original value of the field and can modify it as it sees fit.

In this example, we have an add1 decorator, which returns an initializer function.

This initializer function is called every time the class is instantiated and passed the initial value of the field, which in this case is 1.

It then adds 1 to that value, returns it, and the value is then defined on the class instance, resulting in a value of 2 being defined on the final instance.

Finally, we have auto accessors, the new type of element that I mentioned earlier.

Auto accessors solve a particular problem, which is that oftentimes decorators want to intercept access to a class field, that is, they want to run code whenever they get or set the field.

This can be used to make a field that is observable or reactive, for instance.

However, in this proposal, we can only replace a value with another value of the same type, and class fields do not have the ability to intercept access.

With defined semantics, they overwrite any getters or setters that are either on the prototype or on the instance ahead of them.

Enter auto accessors and the accessor keyword.

When applied to a class field, the accessor keyword transforms the field into a getter and setter with the same name as the field, backed by private storage on the instance.

You can roughly think about this as syntactic sugar for a getter and setter that get and set a private class field.

Auto accessors give us a single value that can be decorated and replaced all together at once, allowing us to solve this use case.

The accessor keyword also has uses other than decoration.

Defining a field like value on the prototype for instance, but it is highly related to decoration and is required for many existing decorators to migrate, which is why it has been included in this proposal.

Decorators for auto accessors receive an object with get, and set functions on it and can return an object with get, set and init functions.

Get and set, replace the getter and setter on the class.

And init decorates the private storage of the auto accessor, much like a class field decorator would.

In this example, we have a decorator which logs whenever we get or set the auto accessor and also adds one whenever the accessor is initialized or updated.

That covers all the different kinds of decoratable values.

Now let's look at the second parameter passed to the decorators, the context object.

This object provides a variety of different contextual values, which can help during decoration.

Starting from the top.

We first have the kind of the decorated value.

This matches the value being decorated and allows the decorator to change behavior based on what it is applied to, which makes it possible to write a single decorator that can be applied to many different types of values.

Next up, we have the name of the value-for classes and public elements, this is the string or symbol name of the value that you can use to access it.

And for private fields, this is the name, as it appears in code.

This value is primarily useful for debugging purposes in order to give developers some hint as to what went wrong, if something goes wrong while decorating.

Next, we have private and static.

These are booleans, which are only present for class elements.

And they tell us whether or not the element is private and whether or not it is static respectively.

After that is access, which is also only present for class elements.

Access provides a get and set function, which can access the value on the class instance.

This is useful for being able to set values, especially private values.

Because there is no other way, external to the class, to be able to get or set a private field or method.

And it can be used for instance, for testing or dependency injection purposes.

One thing to note here is that like decorated methods, the `this` context of these functions is generally important.

And so they need to be called with function dot apply on an instance of the class to function properly.

Last we have addInitializer.

This function allows us to add an additional piece of initialization code to the class that is related to the decorated value.

This code will run whenever the value being decorated is initialized.

So for classes it runs after the class has been fully defined.

For static elements, it runs prior to static classfield initialization.

And for instance elements, it runs prior to instance classfield initialization.

A great example of why addInitializer is so useful is for creating a bound decorator, which binds a method to an instance.

The binding code needs to run for every instance of the class, but normally methods don't have the ability to do that.

With decorators and addInitializer, we can do it by adding an initializer function, which gets the final value of the method via access dot get, and then sets the value of the method to the bound version.

This function runs for every instance of the class and the `this` keyword refers to the instance here.

Now you may be wondering, can decorators receive other arguments besides the value and context object?

Say, if you wanna pass some extra information to them.

The answer is yes and no.

You can provide arguments to a decorator function expression, like in this example, but these arguments are not passed directly to the decorator function.

Instead, what we are doing here is calling a function that returns a decorator, and then applying that decorator function to the field and that decorator function can maintain your arguments in its local scope.

The syntax for expressions in decoration is a bit restricted here in order to prevent ambiguity with automatic semi colon and insertion.

You can chain property accesses and call the last property access in the decorator as a function.

But in order to do more complex expressions, like dynamic property access or chaining functions, you have to escape the whole expression with parentheses.

And that about sums up the majority of the proposal.

There are of course, many small details and edge and corner cases, but we unfortunately don't have enough time to talk through everything today.

One last thing we do need to go over, however, are the main ways that this proposal differs from the previous one.

Decorators had an unusually high, early adoption rate prior to stage three, as I mentioned before, and many folks in the community will need to find an upgrade path.

We don't have time for every difference between every iteration, so I'll just be covering the big ones for the TypeScript and Babel legacy decorators, which were the ones with the most adoption.

Number one, decorators can no longer change the type of a value.

A decorator can't take a field and turn it into a getter and setter or a method into a field.

As mentioned earlier, this is why we have the new `accessor` keyword, which is another major difference.

Any decorators which need to intercept field access will need to update to using this new keyword.

Number two.

Decorators can no longer change their property descriptor.

You can't change the enumerability, configurability or writeability of a decorated value.

This helps to preserve the performance characteristics of classes and prevents unintentional shape changes.

Number three.

Element decorators no longer have access to the class.

This keeps decorators focused and single purpose and prevents them from adding additional properties or methods to the class beyond the decorated value.

It also means that they cannot associate metadata with the class anymore.

Making libraries like reflect.metadata difficult to replicate with some additional ergonomic issues.

There is an ongoing effort to address this with an additional proposal for adding decorator metadata currently in stage two.

Number four.

For class decorators, decorators are now placed after the export keyword.

This is because the decoration is being applied to the class definition as a whole.

And is it is more consistent with the way class expressions are defined.

For instance, decorators also come after the return keyword if you return a class definition from a function.

And lastly, Decorator expressions are more restricted.

As we covered before more complicated decorator expressions will need to be wrapped in parentheses, which is a fairly straightforward, good mod.

These are going to be the biggest changes that will cause some churn for early adopters.

However, in my experience, the trade offs have been more than worth it with increased clarity and simplicity across the board.

Overall, I'm really excited to see this proposal finally, moving forward, giving us the long awaited ability to metaprogram with JavaScript classes.

If you're interested in giving them a shot, definitely try them out with Babbel or TypeScript as the transforms become available.

And pretty soon you'll be able to try them behind feature flags in the major run times, too.

It's been a long and hard journey, but it's nearly completed and looking back, I think the language is gonna be in a better place for it.

Thank you.

And I hope you're excited about decorators too.

Decorators

Stage 3 (Finally!)

What is a decorator?

function add(a, b) {
  console.log(`calling add, args:`, ...args);

  const result = a + b;

  console.log(`finished add, result:`, result);

  return result;
}

function sub(a, b) {
  console.log(`calling sub, args:`, ...args);

  const result = a - b;

  console.log(`finished sub, result:`, result);

  return result;
}
function debugLog(fn) {
  return (...args) => {
    console.log(`calling ${fn.name}, args:`, ...args);
    
    const result = fn(...args);
    
    console.log(`finished ${fn.name}, result:`, result);
    
    return result;
  };
}

const add = debugLog((a, b) => a + b);
const sub = debugLog((a, b) => a - b);
const C = decorate1(
  decorate2(
    class {
      // ...
    }
  )
);
class C {
  // ???
  method() {}
}

Decorators as of March 2022

	
function dec(value, context) {
  // apply decoration
}
 
 
@dec
class C {
  @dec m() {}
  
  @dec get x() {}
  @dec set x(v) {}
  
  @dec y = 123;
  @dec accessor z = 456;
}
 
 
@second
@first
class C {
  @second @first x = 123;
}
function dec(Class) {
  return class Decorated extends Class {
    // ...
  }
}
 
@dec class C {}

function logResult(method) {
  return function (...args) {
    const result = method.apply(this, args);
 
    console.log(result);
 
    return result;
  }
}
 
class C {
  @logResult m() {
    return 123;
  }
}
 
new C().m(); // logs "123"

function add1() {
  return function (value) {
    return value + 1;
  }
}

class C {
  @add1 x = 1;
}

console.log(new C().x); // 2
class C {
  accessor x = 123;
}

// is approximately the same as

class C {
  #x = 123;

  get x() {
    return this.#x;
  }

  set x(value) {
    this.#x = value;
  }
}
function addOneAndLog({ get, set }) {
  return {
    get() {
      const value = get.call(this);
      console.log('getting value:', value);
      return value;
    },
    set(value) {
      console.log('setting value:', value + 1)
      set.call(this, value + 1);
    },
    init(value) {
      return value + 1;
    }
  }
}

class C {
  @addOneAndLog accessor x = 1;
}

const c = new C();

c.x; // getting value: 2;
c.x = 2; // setting value: 3;
interface DecoratorContext {
  kind: 'class' | 'method' | 'getter' | 'setter' | 'field' | 'accessor';
  
  name: string | symbol;
  
  private?: boolean;
  
  static?: boolean;
  
  access: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  
  addInitializer?(initializer: () => void): void;
}
function bound(_, { access, addInitializer }) {
  addInitializer(function () {
    // get the final method
    const method = access.get.call(this);

    // set it to a bound version
    access.set.call(this, method.bind(this));
  });
}

class C {
  @bound m() {}
}
class C { @inject('foo') bar; }
function inject(name) {
  return function injectionDecorator() {
    // ...injection logic using name
  }
}

class C {
  @inject('foo') bar;
}
class C {
  // valid
  @foo.bar x;
  @foo.bar() x;
  @(foo['bar']().baz) x;

  // invalid
  @foo['bar'] x;
  @foo().bar() x;
}

Major Differences

Between March 2022 and Babel Legacy/Typescript

1. Decorators cannot change the type of a decorated value.

2. Decorators cannot change the property descriptor for class elements.

3. Class element decorators do not have access to the class itself.

4. Class decorator come after the export keyword

// before
@dec export class Foo {
  
}

// after
export @dec class Foo {
  
}

function makeAClass() {
  return @dec class Bar {
    // ...
  }
}

5. Decorator expressions are more restricted.

// before
export class Foo {
  @foo().bar() x; 
}

// after
export @dec class Foo {
  @(foo().bar()) x
}


Major Differences

  1. Decorators cannot change the type of a decorated value.
  2. Decorators cannot change the property descriptor for class elements.
  3. Class element decorators do not have access to the class itself.
  4. Class decorator come after the export keyword
  5. Decorator expressions are more restricted.

Looking Forward

Thank you!

pzuraq.com