JavaScript Decorators and Auto-Accessors
Tags: javascript, webdev
Published: 2024-07-10 01:30:30

A walkthrough of how to create JavaScript decorators and how using auto-accessors helps improve your developer experience. 

## Table of Contents

- [Context and Specification](#context-and-specification)
- [Preface](#preface)
- [Auto-Accessors](#autoaccessors)
- [Creating Decorators](#creating-decorators)
    - [A Simple Decorator](#a-simple-decorator)
    - [Validation With Decorators](#validation-with-decorators)
    - [Decorator Options](#decorator-options)
- [Metadata](#metadata)

## Context and Specification

The [Decorators Proposal on GitHub](https://github.com/tc39/proposal-decorators) already does a great job of breaking down the basic use-cases of decorators. My goal isn't to recreate those examples there, but instead to highlight some lesser-known features and interactions. Additionally, in the next article in this series I'll highlight how to compose or chain multiple decorators on a single class property.

## Preface

Each code sample will come with a link to an interactive [Babel REPL playground](https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=Q&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.24.7&externalPlugins=&assumptions=%7B%7D), so you can try it for yourself without needing to set up a polyfill or spin up a repo. The "Evaluate" option in the top left (under `Settings`) should be checked in all my examples, which means that you will be able to see the code, edit it, open your browser's dev console, and see the logs / results there. 

**You don't need to pay attention to the transpiled code on the right-hand side of the Babel REPL**, unless you want to dig into the polyfill for decorators. The left-hand side of the Babel REPL is where you can edit and write code to try out for yourself.

**To emphasize, your developer tools' console should show console logs. If it doesn't, make sure that `Evaluate` is checked in the top left.**

## Auto-Accessors

An important feature of the Decorators spec are auto-accessors. We'll start with learning what they are and how using auto-accessor will make writing decorators easier.

The the Decorators Proposal [outlines auto-accessor here](https://github.com/tc39/proposal-decorators?tab=readme-ov-file#class-auto-accessors). But ultimately it's a simple feature; let's look at a basic working example: [Babel REPL](https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=MYGwhgzhAECyCeBhcVoG8BQ1pmMAplAPYBO0AtvAEJFEj5gB20AvNAGZggT4YC-GDMCKMIAF2hj85AA6tojfAHc4SFBAAUASiEiIdfADoQRAOYaA5OwCWJcRYA0k6TMOUaBpjoxTZb6rT0TPJiJACu-ADcuqIGxmaWPMKMACaOzn7ugQyMWkA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.24.7&externalPlugins=&assumptions=%7B%7D).

```js
class MyClass {
  accessor myBoolean = false
}
```

In this class definition the `accessor` keyword goes before the property name. However, this hasn't really changed anything about the property yet - next, we'll see how useful auto-accessors are when combined with decorators.

(Note that you can also use `static` with auto-accessors, such as `static accessor myBoolean = false`)

## Creating Decorators

To better understand why we're using an auto-accessor, let's build some decorators.

### A Simple Decorator

We'll start by combining auto-accessors with a decorator that doesn't actually do much, in order to get an idea of the syntax.

Here's a functional decorator that keeps an internal variable, and allows you to get and set that variable through the property on the class: [Babel REPL](https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=GYVwdgxgLglg9mABAZxgWwA4BsCmARHCOAJwEMoSAKAN1KxBwBpEiwocAPKASkQG8AUIkS4oiGGxzEwdAGp0GiALyJgdZDiGJiOKCGn8twgOa7KvQcKvbd-pBPbS5CzdYC-jIyjO0sFr8IOUjJY8vQ4yoi-ATZ6BkFOoS5eblqpqQIQWKTIyIgAsgCeAMLZuYbCAAKomLgERGQUxFqkEBA4uSSIaIUAQnBwuKRgAhmsyGLsmJFgOADuBSVlyOaZCMiDOAB0WHDGlADkwDDEEyJ7B8xTGFs9_ZvD3ALXt30DQ0gqUMQMANxrYA2uB2e0OGlYABNzsZLogXnd3jhHgIBEA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.24.7&externalPlugins=&assumptions=%7B%7D)

```js
function simpleDecorator(value, context) {
  let internalValue = false
  return {
    get() {
      return internalValue
    },
    set(val) {
      internalValue = val
      return internalValue
    }
  }
}

class MyClass {
  @simpleDecorator
  accessor myBoolean
}
```

This decorator returns an object with two methods: `get()` and `set()`. This is how a decorator for an auto-accessor can "decorate" or wrap both the setter and the getter for a property in a single place; we don't have to create a `simpleGetterDecorator` and `simpleSetterDecorator`. Instead, we've combined them into a single definition with auto-accessors, which is easier.

In the end, this looks like a fairly normal function so far - which is great for an introduction!

### Validation With Decorators

To set us up for the rest of the article, let's update our decorator so that it actually does some sort of validation. We'll make a decorator that only allows you to set even numbers and nothing else. Here's what that would look like: [Babel REPL](https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=GYVwdgxgLglg9mABAgNgTwKIDcCmYByIAtgEY4BOAzgBRYCGKIOANIhAlDgB5QCUiAbwBQiRChxREMMJ3JgGhUhUQBeRAAYRichJBzBW0QHMJ1fsNGXtu_dNnyUisuUOIAvs1eVT9FOdei7GCUkmDEqohOFLQMvAFSwNQwlPh0-NRhRLz-VlYA9HmIACYIAOSS3pJQABY4iL5MCVJQpZSIYHCSdO3EzvGiOlB6SHYUDlEuue7xMImZiACkiABMiACEKmrqOVOIBcVlXSgocADuyEVFPUpUQgCQd4PDUjJjCr0U8W4zr3LvNxFfPEnrZfuMPpNLN9RN9vkIICg6JQ2gBZNAAYURyIMogAAqhMLgCBDKFo6BAIDhkXByIgiIS8BMhHCgiFEJwiAAHCJgHDnNGYpE0OKsuDiAB0JyM1FKRRwwDoIBQkgaOFKrA5nPF9OwjIhcSEmu1DOJALUUHITHhCEoYpwkrg0tKdGAsnZ5DQ0iM7LgiEqL18MCuqvV7JwXONutNzgNRp1RImEQAzNbgnaHU6XW6LZ6wN6oL7_QWLldMs5Q3GTRNY-GtfG9WbEAAWVO2iVSmVZ5SVWB5n2IHBE67ljW1yMJ_VCIRAA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.24.7&externalPlugins=&assumptions=%7B%7D)

```js
function onlyEvenNumbers(value, context) {
  let internalNumber = 0
  return {
    get() {
      return internalNumber
    },
    set(val) {
      const num = Number(val)
      if(isNaN(num)) {
        // don't set the value if it's not a number or coerced to a number
        return internalNumber
      }
      if(num % 2 !== 0) {
        // don't allow odd numbers
		return internalNumber
      }
      internalNumber = val
      return internalNumber
    }
  }
}

class MyClass {
  @onlyEvenNumbers
  accessor myEvenNumber
}
```

So we add logic to the `set()` method and now anyone trying to set the `myEvenNumber` property on our class will go through that validation logic. Nice.

### Decorator Options

Now that we have a nice only-evens decorator, let's make it handle both even and odd numbers with an option to configure which type of number we want!

Fortunately, because this is fairly-normal-looking JavaScript we're writing here, it's not too hard to configure it to work this way. We wrap the original decorator with a function that takes in an option, and then return the decorator. [Babel REPL](https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=GYVwdgxgLglg9mABAUwG7LAZwPICdsAmBmAFAgDYCeAoulogLyJS4jICUiA3gFCKK5kUELiShIsBIgLIIcXAEMo8kqgXk2AGkRywUZAA8onXvwCQ5IYhh7ko9QDkQAWwBGdxogAMfRGcHCoty-_PwA5kIkJiGh_kIiSDb69uRObnYx_AC-mpmImJFq5NGhsbqYUIhgLp5p7riq6ux5ZjDAJDCYDgoOJNXO7CWlsQD0I9IIAOSVBZVQABbIiEVs1sDWUJOYVXCVClUu9XnmAQnWtil1GcN-WS1tfTUApIgATIgAhAxMZGBUtBhtgB-byIABciAAjINgmY4acgkk7GBHIdrsMzHcbq0LijUmjcJ4iscBPFEbjUelcHksbdfHc7jwIOQFJhtgBZSgAYRZbOC_AAAmhAXhCMQSCw2M1-AoIBBkGz5IhnDQ6FdqfxfEK6Dh8ERSMB1AVpYhZfLFYSVWL1TxGeU5shnAAHTxgZAAd0QnJ5rNIzXKcEsADpyHAwiRJsKwBCZIaQORKitkJNtPpnUGVQCwOrmjw006M6qMOrPJNFuRQ5MmQhMIHkCGwxGoxCFMBksxcJQbGFmHB8lYbEUYARluo2CnmI6C5m1QTc_nC1mS0wAMzVrB1hvhyN0FttjwsLtgHvKftzPtwIgHKkThcz4tznh5qeL2dUzwAFnXteDoe3zdNfdCVmWBj17FA6Gvepbxfe9s0fHgxkQABaVC0LQp8A1_RtJkvAgY2QOME1HDRk1TWDKGtBC70oohl0QMtkArOAqyw-s_wjPC93bQ9u3A2ZziHEckxg9MrTo6iKKo99V2_TcONwohuIPTs-NPATTzwqC7FE6daIIHMnxo6T6k_OTsO3LjAPbEC1L7KNtNwXTCxMux2AAbiAA&debug=false&forceAllTransforms=false&modules=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Creact%2Cstage-2&prettier=false&targets=&version=7.24.7&externalPlugins=&assumptions=%7B%7D)

```js
function evensOrOdds(onlyEvens = true) {
  return function decorator(value, context) {
  	let internalNumber = 0
  	return {
      get() {
      	return internalNumber
      },
      set(val) {
      	const num = Number(val)
      	if(isNaN(num)) {
        	// don't set the value if it's not a number
        	return internalNumber
      	}
      	if(num % 2 !== (onlyEvens ? 0 : 1)) {
			return internalNumber
      	}
      	internalNumber = val
        return internalNumber
      }
  	}
  }
}

class MyClass {
  @evensOrOdds(true)
  accessor myEvenNumber
  
  @evensOrOdds(false)
  accessor myOddNumber
}
```

We've now configured our decorator to take in arbitrary options, which allows users of our decorator to customize its behavior. Yay.

## Metadata

One additional tool your decorators can utilize is `context.metadata`. This object is passed to each decorator and you could use it for a variety of things, but you need to be careful because the metadata object is the same for all invocations of every decorator.

## Continue Learning

Continue to [the next post in the series](https://dev.to/frehner/composing-javascript-decorators-2o38) to learn how to compose (or apply multiple) decorators to a single property!