Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates proposal to the latest design #12

Merged
merged 2 commits into from
Mar 7, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
pzuraq marked this conversation as resolved.
Show resolved Hide resolved
}) => 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.
pzuraq marked this conversation as resolved.
Show resolved Hide resolved

- 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;
pzuraq marked this conversation as resolved.
Show resolved Hide resolved
};
}

@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;
pzuraq marked this conversation as resolved.
Show resolved Hide resolved
};
}

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