Decorators Update
Chris Hewell Garrett
2021-07
1 Proposal Overview
2 Current Status
- Decorators are plain JavaScript functions
- Can be applied to
- Classes
- Class elements
- Three main capabilities
- Replacement
- Metadata
- Access
- Additional capabilities via modifiers (e.g. `@init:` syntax)
- Also includes a new class element: auto-accessors
Proposal Overview
function decorate() {
// ...
}
@decorate
class Example {
@decorate myField = 123;
@decorate accessor myOtherField = 456;
@decorate myMethod() {
// ...
}
@decorate get myAccessor() {
// ...
}
@init:decorate myOtherMethod() {
// ...
}
}
Decorator Function API
function decorate(
value: Input,
context: {
kind: string;
name: string | symbol;
access?: {
get?(): unknown;
set?(value: unknown): void;
};
isPrivate?: boolean;
isStatic?: boolean;
addInitializer?(initializer: () => void): void;
getMetadata(key: symbol);
setMetadata(key: symbol, value: unknown);
}
): Output | void;
- First argument is value being decorated
- Second argument is context object providing information about type of value + additional APIs
- Return value is the replacement value
- Input and Output type depend on value that is being decorated
Decorator Capabilities: Replacement
Decorators may replace the value that they are syntactically applied to with another value of the same type
They cannot:
- Class decorators return a new class, method decorators a new method, etc.
- Field decorators return pipelined initializers (do not receive initializer in decorator itself)
- Replace with a different type of value
- Add, remove, or replace values other than the decorated value
function logged(method) {
return (...args) => {
console.log(`${method.name} called`);
return method(...args);
}
}
class MyClass {
// myMethod is replaced with the
// logged version
@logged
myMethod() {
// ...
}
}
// Will throw an error because the
// decorator does not return a constructor
@logged
class MyOtherClass {}
Decorator Capabilities: Metadata
Decorators may add metadata to an element which can then be read later and used by other readers/libraries
const VALIDATIONS = Symbol();
function isString(method, context) {
context.addMetadata(
VALIDATIONS,
(v) => typeof v === 'string'
);
}
function validate(obj) {
let validations = obj[Symbol.metadata][VALIDATIONS];
for (let key in validations.public) {
let validator = validations.public[key];
if (!validator(obj[key])) {
return false;
}
}
return true;
}
class MyClass {
@isString someString = 'foo';
}
- Metadata is accessed via `Symbol.metadata`
- Metadata must be namespaced under a symbol, to help prevent collisions
- Metadata is inherited
Decorator Capabilities: Access
Decorators may provide access to an element to collaborative code that works with the decorator, usually via metadata
- Decorators on public values provide access via the string name of the value
- Decorators on private values provide access via a `get` and `set` function which can be used to access the value
const EXPOSE = Symbol();
function expose(val, context) {
let { name, access } = context;
context.setMetadata(EXPOSE, { name, access });
}
class MyClass {
@expose #value = 123;
}
test('Private field is a number', (assert) => {
let { private } = MyClass[Symbol.metadata][EXPOSE];
let { get } = private.find((m) => m.name === '#value');
let instance = new MyClass();
assert.equal(typeof get.call(instance), 'number');
})
Decorator Modifiers: @init
Users can prefix a decorator with `@init:` to add the ability to add initializers to the value
- Initializers added via `addInitializer` on context obj
- Class element initializers run during class construction for each instance of the class
- Class initializers run after the class has been fully defined (e.g. after static fields have been assigned)
function bound(fn, { name, addInitializer }) {
addInitializer(function() {
this[name] = this.name.bind(this);
});
}
class MyClass {
message = 'Hello!';
@init:bound callback() {
console.log(this.message);
}
}
let { callback } = new MyClass();
callback(); // Hello!
New Class Element: Auto-Accessor
Auto-accessors are similar to class fields, but have a getter and setter defined instead
- Can be used to make a "field" on the prototype
- Can also be decorated directly, allowing decorators to provide reactivity and intercept access
- Can possibly be extended in the future with a more general syntax (see proposal here)
class MyClass {
accessor someValue = 123;
}
// Similar to
class MyClass {
#someValue = 123;
get someValue() {
return this.#someValue;
}
set someValue(value) {
this.#someValue = value;
}
}
// Can be used to intercept gets/sets
class MyClass {
@reactive accessor someValue = 123;
}
- Decorators champion group has internal consensus with the details of the current proposal
- Spec has been written, is available at:
- Experimental transpiler has been implemented:
- Babel plugin in the process of being updated
- Babel plugin will add additional community feedback from a larger audience
- Once the Babel plugin is released and has had some time for testing, will propose for stage 3
- Seeking reviewers for Stage 3
Current Status
Questions?
@Thanks!
Decorators Update 2021-07
By pzuraq
Decorators Update 2021-07
- 1,846