@decorators

Richard Lindsey     @Velveeta

General Function Decoration

Decoration is a software pattern that uses a higher order function. The original function is passed to this higher order function, which "decorates" it in a layer that adds some additional functionality, and then invokes the original target function with the passed parameters.

const sourceFunction = (a, b) => {
  return someComputedResult(a, b);
}

const decoratorFunction = fn => (...args) => {
  someAdditionalTask();
  return fn(...args);
}

const decoratedSourceFunction = decoratorFunction(sourceFunction);

sourceFunction(param1, param2); // sourceFunction executes in normal fashion
decoratedSourceFunction(param1, param2); // sourceFunction executes with someAdditionalTask

What is a decorator?

  • TC39 Stage 2 proposal
  • Behavioral or function abstraction
    • @readonly
    • @autobind
    • @frozen
  • A declarative way of mixing in features
    • @debounce
    • @memoize
    • @logger

What is a decorator?

import debounce from 'lodash.debounce';
import React  from 'react';

const MySearchQueryComponent = class extends React.Component {
  constructor(...args) {
    super(...args);
    
    this._onKeyPress = debounce(this._onKeyPress, 300);
  }
  
  _onKeyPress = (e) => {
    this.props.onSearch(e.target.value);
  };

  render() {
    return <input type="text" onKeyPress={this._onKeyPress} />;
  }
}

What is a decorator?

import { @autobind, @debounce } from 'shared/decorators';
import React  from 'react';

const MySearchQueryComponent = class extends React.Component {
  @debounce(300)
  @autobind
  _onKeyPress(e) {
    this.props.onSearch(e.target.value);
  };

  render() {
    return <input type="text" onKeyPress={this._onKeyPress} />;
  }
}

The Ever-changing Spec

Legacy Edition (Demo)

  • Allowed for applying decorators to classes, or properties of classes, whether they held values or functions.
  • Decorating functions and properties
    • target: The prototype reference housing the function or variable.
    • name: The key of the function or variable within the target.
    • descriptor: The property descriptor, the same as what might be passed to Object.defineProperty.
  • Decorating classes
    • Receives the class reference as the only parameter. Do whatever direct modifications you want for that reference.
    • No way to modify constructor function.

The Ever-changing Spec

Current Edition (Demo)

  • Allowed for applying decorators to classes, or properties of classes, whether they held values or functions.
  • Unified interface for classes and class members
    • Single 'Descriptor' parameter: Contains variety of properties for things like 'kind', 'elements', 'key', 'placement', 'descriptor', and 'initializer' properties, to be used to construct an output Descriptor that describes how this item is actually applied.

The Ever-changing Spec

Future Edition

  • Built-in decorators to compose into more complex decorators:
    • @wrap: Replace a method or the entire class with the return value of a given function.
    • @register: Call a callback after the class is created.
    • @expose: Call a callback given functions to access private fields or methods after the class is created.
    • @initialize: Run a given callback when creating an instance of the class.
    • More built-ins may be coming to allow for even more functionality.
  • Declared, imported, and exported with the 'decorator' keyword and @ prefix on the name of the decorator function itself.

The Ever-changing Spec

Future Edition

export decorator @logged {
  @wrap(f => {
    // Pull the function name property
    const name = f.name;
    
    // Create a new version of our function, wrapped with additional functionality
    function wrapped(...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      f.call(this, ...args);
      console.log(`ending ${name}`);
    }
    
    // Define a 'name' property on our new wrapped function, in case another @wrap is applied
    Object.defineProperty(wrapped, "name", {
      value: name,
      configurable: true
    });
    
    // Return our wrapped function as the function definition for this decorated item
    return wrapped;
  })
}

The Ever-changing Spec

Future Edition

import { @logged } from './logged';

class C {
  @logged
  setPrivateMember(arg) {
    this.#privateMember = arg;
  }

  @logged
  set #privateMember(value) { }
}

new C().setX(1);
// starting method with arguments 1
// starting set #privateMember with arguments 1
// ending set #privateMember
// ending method

The Ever-changing Spec

Future Edition

export decorator @frozen {
  // This decorator fires upon class creation (not instantiattion)
  @register(klass => {
    // When we create this class, freeze it from extention
    Object.freeze(klass);
    
    // Then iterate over any static properties of the class and freeze each one
    for (const key of Reflect.ownKeys(klass)) {
      Object.freeze(klass[key]);
    }
    
    // Then iterate over all keys of the class prototype and freeze each one
    for (const key of Reflect.ownKeys(klass.prototype)) {
      Object.freeze(klass.prototype[key]);
    }
  })
}

The Ever-changing Spec

Future Edition

import { @frozen } from "./frozen";

@frozen
class MyClass {
  method() { }
}

MyClass.method = () => {};            // TypeError to add a method
MyClass.prototype.method = () => {};  // TypeError to overwrite a method
MyClass.prototype.method.foo = 1;     // TypeError to mutate a method

Why use decorators?

  • Decorators allow  you to isolate reusable behaviors that can be applied to fields, methods, or classes in a general sense, without having to implement those behaviors everywhere they're desired.
  • Decorators can be individually tested just like any other piece of code, to confirm that the intended side-effects are present when applied.
  • In the absence of multiple inheritance, decorators are the right solution for mixing in behaviors on an ad-hoc basis.
    • Even in languages that allow for multiple inheritance, inheritance tightly couples a class to a dependency (its superclass). Decorators allow for much more granular application of mixed in behaviors, and are composable.

Why use decorators?

Instead of this:

class Foo {
  constructor() {
    Object.defineProperty(this, 'bar', {
      configurable: true,
      enumerable: true,
      writeable: false,
      value: true,
    });
  }
}

Why use decorators?

You should do this:

export decorator @readonly {
  @initialize((instance, name, value) => {
    Object.defineProperty(instance, name, {
      configurable: true,
      enumerable: true,
      writable: false,
      get() { return value; },
    });
  })
}

Why use decorators?

And then this:

import { @readonly } from './decorators';

class Foo {
  @readonly bar = true;
}

Why use decorators?

Want to test it?

import { @readonly } from './decorators';

describe('decorators', () => {
  describe('@readonly', () => {
    it('should create a readonly field on a class member', () => {
      class Test {
        shouldBeWritable = true;
      
        @readonly shouldNotBeWritable = true;
      }

      const test = new Test();

      // Assert for normal behavior
      expect(() => { test.shouldBeWritable = false }).not.toThrow();

      // Assert for readonly behavior
      expect(() => { test.shouldBeWritable = false; }).toThrow();
    });
  });
});

When can you use decorators?

  • If you want to use either of the outdated versions of the spec, you can use them today!
  • If you want to use the current version of the spec, I don't know!
    • Currently, it's in a situation of limbo, with supported plugins for outdates versions of the spec, and no support for the current version. Eventually, this should get its own babel transform.
  • In either case, authoring decorators now will allow you to start using their @decorator syntax throughout the codebase for classes and properties, and when the current spec has a supported transform, only the source modules should need to be modified.

fn.

Richard Lindsey     @Velveeta

@decorators

By Richard Lindsey