JavaScript Decorators in GraalJS

JavaScript Decorators in GraalJS

What are the JS decorators?

Decorators in JavaScript could be thought of as a mechanism of wrapping one "block" of code with another adding additional behavior. People familiar with the Decorator pattern should easily grasp the idea of decorators. As stated in the TC39 specification for the decorators, they could be described as having three primary capabilities:

  1. They can replace the value that is being decorated with a matching value that has the same semantics. (e.g. a decorator can replace a method with another method, a field with another field, a class with another class, and so on).

  2. They can provide access to the value that is being decorated via accessor functions which they can then choose to share.

  3. They can initialize the value that is being decorated, running additional code after the value has been fully defined. In cases where the value is a member of class, then initialization occurs once per instance.

Essentially, decorators can be used to metaprogram and add functionality to a value, without fundamentally changing its external behavior.

A simple example of a decorator could be the following one:

function logged(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`Calling ${name} with arguments ${args.join(", ")}`);
      return value.call(this, ...args);
    };
  }
}

class DecoratedClass {
  @logged
  test(arg) {}
}

const decorated = new DecoratedClass();
decorated.test(123);

In this example the @logged decorator simply returns a new function that wraps the original one and logs a message when it's called. So, when you call decorated.test(123) you would see the message Calling test with arguments 123 in the console. Have in mind this decorator is a method decorator. There are different types of decorators having different kind:

  • class

  • method

  • getter

  • setter

  • field

  • accessor

State of decorators support

Many people may be familiar with decorators from TypeScript. They are used extensively in frameworks like Angular 2+ and NestJS in order to reduce boilerplate code and improve the readability of various patterns. Although decorators have been used in TypeScript for years, they are yet to be released in the ECMAScript specification. At the time of writing this blog post, they are at a Stage 3 proposal. As decorators are still an experimental feature of JavaScript, most engines and many build tools do not yet support them. Still, some engines have added initial support enabling users to try them out. One such engine is GraalJS

Using decorators in GraalJS

Let's try to build a simple console application using GraalJS. You could head over to your favorite Java IDE and create a new project. You could also check the code of my repository. Personally, I use IntelliJ IDEA and Gradle as my preferred setup so that's what I've used in my project.

Adding the GraalJS dependency

The first thing we need for using GraalJS is to add its dependency. At the time of writing, the latest version is 22.3.0. In Gradle's build.gradle, you could add it with:

implementation 'org.graalvm.js:js:22.3.0'

and in Maven's pom.xml:

<dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js</artifactId>
    <version>22.3.0</version>
</dependency>

Writing the code

Now that we have the necessary dependency, we could create a simple Java main function. In my example repository, you could see I have created the com.vmutafov.Main class.

The first thing we should do is to create a JS Context:

var context = Context
    .newBuilder("js")
    .option("js.ecmascript-version", "staging")
    .option("engine.WarnInterpreterOnly", "false")
    .build();

The GraalJS Context allows us to evaluate code. In this case, we have configured it only for evaluating js using the newBuilder("js") method.

The js.ecmascript-version option, at least for now, must be set to staging in order to use the JS decorators.

The engine.WarnInterpreterOnly option simply tells GraalJS not to log a warning when we are using a regular JDK instead of the GraalVM. You could remove this option and see what happens.

The next thing we now need is the JavaScript we are going to evaluate. We could reuse the decorators example from the start of this blog post:

String js = """

        function logged(value, { kind, name }) {
          if (kind === "method") {
            return function (...args) {
              console.log(`Calling ${name} with arguments ${args.join(", ")}`);
              return value.call(this, ...args);
            };
          }
        }

        class DecoratedClass {
          @logged
          test(arg) {}
        }

        const decorated = new DecoratedClass();
        decorated.test(123);

        """;

Now that we have our code, we should evaluate it:

context.eval("js", js);

When you now run the program, you should see Calling test with arguments 123 logged in to the console.

Wrap up

To summarize, your Main class should be similar to this one:

package com.vmutafov;

import org.graalvm.polyglot.Context;

public class Main {
    public static void main(String[] args) {
        var context = Context
                .newBuilder("js")
                .option("js.ecmascript-version", "staging")
                .option("engine.WarnInterpreterOnly", "false")
                .build();

        var js = """

                function logged(value, { kind, name }) {
                  if (kind === "method") {
                    return function (...args) {
                      console.log(`Calling ${name} with arguments ${args.join(", ")}`);
                      return value.call(this, ...args);
                    };
                  }
                }

                class DecoratedClass {
                  @logged
                  test(arg) {}
                }

                const decorated = new DecoratedClass();
                decorated.test(123);

                """;

        context.eval("js", js);
    }
}

If you followed up the steps above, you should now have a working environment where you could play around with JavaScript's decorators. Feel free to add comments here or in Github's Issues of the repository. Also, do not forget to check out the GraalJS docs to see other awesome features.