@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 someAdditionalTaskWhat 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 methodThe 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 methodWhy 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!
- @babel/plugin-proposal-decorators allows you to use either the legacy version, or the previous version of the spec with a flag.
- 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
@decorators
- 157