GitHub - tc39/proposal-decorator-metadata (original) (raw)
Decorator Metadata
Stage: 3
Spec Text: pzuraq/ecma262#10
This proposal seeks to extend the Decoratorsproposal by adding the ability for decorators to associate metadata with the value being decorated.
Overview
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 aWeakMap
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 theSymbol.metadata
property on the class definition after decoration.
Detailed Design
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'
Inheritance
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']
Private Metadata
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'