Stage: 2
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 value to use as a key to
associate metadata with. This key is then accessible via the
Symbol.metadataKey
property on the class definition.
The overall decorator signature will be updated to the following:
interface MetadataKey {
parent: MetadataKey | null;
}
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;
+ metadataKey?: MetadataKey;
+ class?: {
+ metadataKey: MetadataKey;
+ name: string;
+ }
}) => Output | void;
Two new values are introduced, metadataKey
and class
.
metadataKey
is present for any tangible decoratable value, specifically:
- Classes
- Class methods
- Class accessors and auto-accessors
It is not present for class fields because they have no tangible value (e.g.
there is nothing to associate the metadata with, directly or indirectly).
metadataKey
is then set on the decorated value once decoration has completed:
const METADATA = new WeakMap();
function meta(value) {
return (_, context) => {
METADATA.set(context.metadataKey, value);
};
}
@meta('a')
class C {
@meta('b')
m() {}
}
METADATA.get(C[Symbol.metadata]); // 'a'
METADATA.get(C.m[Symbol.metadata]); // 'b'
This allows metadata to be associated directly with the decorated value.
The class
object is available for all class element decorators, including
fields. The class
object contains two values:
- The
metadataKey
for the class itself - The name of the class
This allows decorators for class elements to associate metadata with the class. For method decorators, this can simplify certain flows. For class fields, since they have no tangible value to associate metadata with, the class metadata key is the only way to store their metadata.
const METADATA = new WeakMap();
const CLASS = Symbol();
function meta(value) {
return (_, context) => {
const metadataKey = context.class?.metadataKey ?? context.metadataKey;
const metadataName = context.kind === 'class' ? CLASS : context.name;
let meta = METADATA.get(metadataKey);
if (meta === undefined) {
meta = new Map();
METADATA.set(metadataKey, meta);
}
meta.set(metadataName, value);
};
}
@meta('a')
class C {
@meta('b')
foo;
@meta('c')
get bar() {}
@meta('d')
baz() {}
}
// Accessing the metadata
const meta = METADATA.get(C[Symbol.metadataKey]);
meta.get(CLASS); // 'a';
meta.get('foo'); // 'b';
meta.get('bar'); // 'c';
meta.get('baz'); // 'd';
Metadata keys also have a parent
property. This is set to the value of
Symbol.metadataKey
on the prototype of the value being decorated.
const METADATA = new WeakMap();
function meta(value) {
return (_, context) => {
const classMetaKey = context.class.metadataKey;
const existingValue = METADATA.get(classMetaKey.parent) ?? 0;
METADATA.set(classMetaKey, existingValue + value);
};
}
class C {
@meta(1)
foo;
}
class D extends C {
@meta(2)
foo;
}
// Accessing the metadata
METADATA.get(C[Symbol.metadataKey]); // 3
Todo