Stage: 3
Spec Text: pzuraq/ecma262#10
This proposal seeks to extend the Decorators proposal by adding the ability for decorators to associate metadata with the value being decorated.
Decorators are functions that allow users to metaprogram by wrapping and replacing existing values. This allows them to solve a number of use cases quite well, such as memoization, reactivity, method binding, and more. However, there are a number of use cases which require code external to the decorator and decorated class to be able to introspect and understand what decorations were applied, including:
- Validation
- Serialization
- Web component definition
- Dependency injection
- Declarative routing/application structure
- ...and more
In previous iterations of the decorators proposal, all decorators had access to
the class prototype, allowing them to associate metadata directly via a
WeakMap
by using the class as a key. This is no longer possible in the most
recent version, however, as decorators only have access to the value they are
directly decorating (e.g. method decorators have access to the method, field
decorators have access to the field, etc).
This proposal extends decorators by providing a metadata object, which can be
used either to directly store metadata, or as a WeakMap key. This object is
provided via the decorator's context argument, and is then accessible via the
Symbol.metadata
property on the class definition after decoration.
The overall decorator signature will be updated to the following:
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
isPrivate?: boolean;
isStatic?: boolean;
addInitializer?(initializer: () => void): void;
+ metadata?: Record<string | number | symbol, unknown>;
}) => Output | void;
The new metadata
property is a plain JavaScript object. The same object is
passed to every decorator applied to a class or any of its elements. After the
class has been fully defined, it is assigned to the Symbol.metadata
property
of the class.
An example usage might look like:
function meta(key, value) {
return (_, context) => {
context.metadata[key] = value;
};
}
@meta('a', 'x')
class C {
@meta('b', 'y')
m() {}
}
C[Symbol.metadata].a; // 'x'
C[Symbol.metadata].b; // 'y'
If the decorated class has a parent class, then the prototype of the metadata
object is set to the metadata object of the superclass. This allows metadata to
be inherited in a natural way, taking advantage of shadowing by default,
mirroring class inheritance. For example:
function meta(key, value) {
return (_, context) => {
context.metadata[key] = value;
};
}
@meta('a', 'x')
class C {
@meta('b', 'y')
m() {}
}
C[Symbol.metadata].a; // 'x'
C[Symbol.metadata].b; // 'y'
class D extends C {
@meta('b', 'z')
m() {}
}
D[Symbol.metadata].a; // 'x'
D[Symbol.metadata].b; // 'z'
In addition, metadata from the parent can be read during decoration, so it can be modified or extended by children rather than overriding it.
function appendMeta(key, value) {
return (_, context) => {
// NOTE: be sure to copy, not mutate
const existing = context.metadata[key] ?? [];
context.metadata[key] = [...existing, value];
};
}
@appendMeta('a', 'x')
class C {}
@appendMeta('a', 'z')
class D extends C {}
C[Symbol.metadata].a; // ['x']
D[Symbol.metadata].a; // ['x', 'z']
In addition to public metadata placed directly on the metadata object, the
object can be used as a key in a WeakMap
if the decorator author does not want
to share their metadata.
const PRIVATE_METADATA = new WeakMap();
function meta(key, value) {
return (_, context) => {
let metadata = PRIVATE_METADATA.get(context.metadata);
if (!metadata) {
metadata = {};
PRIVATE_METADATA.set(context.metadata, metadata);
}
metadata[key] = value;
};
}
@meta('a', 'x')
class C {
@meta('b', 'y')
m() {}
}
PRIVATE_METADATA.get(C[Symbol.metadata]).a; // 'x'
PRIVATE_METADATA.get(C[Symbol.metadata]).b; // 'y'