Skip to content

Commit

Permalink
Merge pull request #12 from tc39/update-proposal-to-latest
Browse files Browse the repository at this point in the history
Updates proposal to the latest design
  • Loading branch information
pzuraq authored Mar 7, 2023
2 parents 5c59245 + a193c0b commit 6a8b75a
Showing 1 changed file with 75 additions and 94 deletions.
169 changes: 75 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

**Stage**: 2

**Spec Text**: https://github.com/pzuraq/ecma262/pull/10

This proposal seeks to extend the [Decorators](https://github.com/tc39/proposal-decorators)
proposal by adding the ability for decorators to associate _metadata_ with the
value being decorated.
Expand Down Expand Up @@ -31,19 +33,16 @@ 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.
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.

## Detailed Design

The overall decorator signature will be updated to the following:

```ts
interface MetadataKey {
parent: MetadataKey | null;
}

type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
Expand All @@ -54,134 +53,116 @@ type Decorator = (value: Input, context: {
isPrivate?: boolean;
isStatic?: boolean;
addInitializer?(initializer: () => void): void;
+ metadataKey?: MetadataKey;
+ class?: {
+ metadataKey: MetadataKey;
+ name: string;
+ }
+ metadata?: Record<string | number | symbol, unknown>;
}) => Output | void;
```

Two new values are introduced, `metadataKey` and `class`.

### `metadataKey`

`metadataKey` is present for any _tangible_ decoratable value, specifically:
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.

- 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:
An example usage might look like:

```js
const METADATA = new WeakMap();

function meta(value) {
function meta(key, value) {
return (_, context) => {
METADATA.set(context.metadataKey, value);
context.metadata[key] = value;
};
}

@meta('a')
@meta('a' 'x')
class C {
@meta('b')
@meta('b', 'y')
m() {}
}

METADATA.get(C[Symbol.metadata]); // 'a'
METADATA.get(C.m[Symbol.metadata]); // 'b'
C[Symbol.metadata].a; // 'x'
C[Symbol.metadata].b; // 'y'
```

This allows metadata to be associated directly with the decorated value.

### `class`

The `class` object is available for all _class element_ decorators, including
fields. The `class` object contains two values:
### Inheritance

1. The `metadataKey` for the class itself
2. 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.
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:

```js
const METADATA = new WeakMap();
const CLASS = Symbol();

function meta(value) {
function meta(key, value) {
return (_, context) => {
const metadataKey = context.class?.metadataKey ?? context.metadataKey;
const metadataName = context.kind === 'class' ? CLASS : context.name;
context.metadata[key] = value;
};
}

let meta = METADATA.get(metadataKey);
@meta('a' 'x')
class C {
@meta('b', 'y')
m() {}
}

if (meta === undefined) {
meta = new Map();
METADATA.set(metadataKey, meta);
}
C[Symbol.metadata].a; // 'x'
C[Symbol.metadata].b; // 'y'

meta.set(metadataName, value);
};
class D extends C {
@meta('b', 'z')
m() {}
}

@meta('a')
class C {
@meta('b')
foo;
D[Symbol.metadata].a; // 'x'
D[Symbol.metadata].b; // 'z'
```

@meta('c')
get bar() {}
In addition, metadata from the parent can be read during decoration, so it can
be modified or extended by children rather than overriding it.

@meta('d')
baz() {}
```ts
function appendMeta(key, value) {
return (_, context) => {
// NOTE: be sure to copy, not mutate
const existing = context.metadata[key] ?? [];
context.metadata[key] = [...existing, value];
};
}

// Accessing the metadata
const meta = METADATA.get(C[Symbol.metadataKey]);
@appendMeta('a', 'x')
class C {}

meta.get(CLASS); // 'a';
meta.get('foo'); // 'b';
meta.get('bar'); // 'c';
meta.get('baz'); // 'd';
@appendMeta('a', 'z')
class D extends C {}

C[Symbol.metadata].a; // ['x']
D[Symbol.metadata].a; // ['x', 'z']
```

### `parent`
### Private Metadata

Metadata keys also have a `parent` property. This is set to the value of
`Symbol.metadataKey` on the prototype of the value being decorated.
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.

```js
const METADATA = new WeakMap();
```ts
const PRIVATE_METADATA = new WeakMap();

function meta(value) {
function meta(key, value) {
return (_, context) => {
const classMetaKey = context.class.metadataKey;
const existingValue = METADATA.get(classMetaKey.parent) ?? 0;
let metadata = PRIVATE_METADATA.get(context.metadata);

if (!metadata) {
metadata = {};
PRIVATE_METADATA.set(context.metadata, metadata);
}

METADATA.set(classMetaKey, existingValue + value);
metadata[key] = value;
};
}

@meta('a' 'x')
class C {
@meta(1)
foo;
}

class D extends C {
@meta(2)
foo;
@meta('b', 'y')
m() {}
}

// Accessing the metadata
METADATA.get(C[Symbol.metadataKey]); // 3
PRIVATE_METADATA.get(C[Symbol.metadata]).a; // 'x'
PRIVATE_METADATA.get(C[Symbol.metadata]).b; // 'y'
```
## Examples
Todo

0 comments on commit 6a8b75a

Please sign in to comment.