2021-07
1 Proposal Overview
2 Current Status
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;
Decorators may replace the value that they are syntactically applied to with another value of the same type
They cannot:
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 {}
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';
}
Decorators may provide access to an element to collaborative code that works with the decorator, usually via metadata
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');
})
Users can prefix a decorator with `@init:` to add the ability to add initializers to the value
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!
Auto-accessors are similar to class fields, but have a getter and setter defined instead
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;
}
Current Status