• Stars
    star
    2,999
  • Rank 15,068 (Top 0.3 %)
  • Language
    TypeScript
  • License
    Apache License 2.0
  • Created over 9 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Prototype for a Metadata Reflection API for ECMAScript

Metadata Reflection API

Installation

npm install reflect-metadata

Background

  • Decorators add the ability to augment a class and its members as the class is defined, through a declarative syntax.
  • Traceur attaches annotations to a static property on the class.
  • Languages like C# (.NET), and Java support attributes or annotations that add metadata to types, along with a reflective API for reading metadata.

Goals

  • A number of use cases (Composition/Dependency Injection, Runtime Type Assertions, Reflection/Mirroring, Testing) want the ability to add additional metadata to a class in a consistent manner.
  • A consistent approach is needed for various tools and libraries to be able to reason over metadata.
  • Metadata-producing decorators (nee. "Annotations") need to be generally composable with mutating decorators.
  • Metadata should be available not only on an object but also through a Proxy, with related traps.
  • Defining new metadata-producing decorators should not be arduous or over-complex for a developer.
  • Metadata should be consistent with other language and runtime features of ECMAScript.

Syntax

  • Declarative definition of metadata:
class C {
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}
  • Imperative definition of metadata:
Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");
  • Imperative introspection of metadata:
let obj = new C();
let metadataValue = Reflect.getMetadata(metadataKey, obj, "method");

Semantics

  • Object has a new [[Metadata]] internal property that will contain a Map whose keys are property keys (or undefined) and whose values are Maps of metadata keys to metadata values.
  • Object will have a number of new internal methods for [[DefineOwnMetadata]], [[GetOwnMetadata]], [[HasOwnMetadata]], etc.
    • These internal methods can be overridden by a Proxy to support additional traps.
    • These internal methods will by default call a set of abstract operations to define and read metadata.
  • The Reflect object will expose the MOP operations to allow imperative access to metadata.
  • Metadata defined on class declaration C is stored in C.[[Metadata]], with undefined as the key.
  • Metadata defined on static members of class declaration C are stored in C.[[Metadata]], with the property key as the key.
  • Metadata defined on instance members of class declaration C are stored in C.prototype.[[Metadata]], with the property key as the key.

API

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// check for presence of an own metadata key of an object or property
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);

// get metadata value of a metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// get metadata value of an own metadata key of an object or property
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);

// get all metadata keys on the prototype chain of an object or property
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);

// get all own metadata keys of an object or property
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);

// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// apply metadata via a decorator to a constructor
@Reflect.metadata(metadataKey, metadataValue)
class C {
  // apply metadata via a decorator to a method (property)
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

Alternatives

  • Use properties rather than a separate API.
    • Obvious downside is that this can be a lot of code:
function ParamTypes(...types) {
  return (target, propertyKey) => {
    const symParamTypes = Symbol.for("design:paramtypes");
    if (propertyKey === undefined) {
      target[symParamTypes] = types;
    }
    else {
      const symProperties = Symbol.for("design:properties");
      let properties, property;
      if (Object.prototype.hasOwnProperty.call(target, symProperties)) {
        properties = target[symProperties];
      }
      else {
        properties = target[symProperties] = {};
      }
      if (Object.prototype.hasOwnProperty.call(properties, propertyKey)) {
        property = properties[propertyKey];
      }
      else {
        property = properties[propertyKey] = {};
      }
      property[symParamTypes] = types;
    }
  };
}

Notes

  • Though it may seem counterintuitive, the methods on Reflect place the parameters for the metadata key and metadata value before the target or property key. This is due to the fact that the property key is the only optional parameter in the argument list. This also makes the methods easier to curry with Function#bind. This also helps reduce the overall footprint and complexity of a metadata-producing decorator that could target both a class or a property:
function ParamTypes(...types) {
  // as propertyKey is effectively optional, its easier to use here
  return (target, propertyKey) => { Reflect.defineMetadata("design:paramtypes", types, target, propertyKey); }

  // vs. having multiple overloads with the target and key in the front:
  //
  // return (target, propertyKey) => {
  //    if (propertyKey === undefined) {
  //      Reflect.defineMetadata(target, "design:paramtypes", types);
  //    }
  //    else {
  //      Reflect.defineMetadata(target, propertyKey, "design:paramtypes", types);
  //    }
  // }
  //
  // vs. having a different methods for the class or a property:
  //
  // return (target, propertyKey) => {
  //    if (propertyKey === undefined) {
  //      Reflect.defineMetadata(target, "design:paramtypes", types);
  //    }
  //    else {
  //      Reflect.definePropertyMetadata(target, propertyKey, "design:paramtypes", types);
  //    }
  // }
}
  • To enable experimental support for metadata decorators in your TypeScript project, you must add "experimentalDecorators": true to your tsconfig.json file.
  • To enable experimental support for auto-generated type metadata in your TypeScript project, you must add "emitDecoratorMetadata": true to your tsconfig.json file.
    • Please note that auto-generated type metadata may have issues with circular or forward references for types.

Issues

  • A poorly written mutating decorator for a class constructor could cause metadata to become lost if the prototype chain is not maintained. Though, not maintaining the prototype chain in a mutating decorator for a class constructor would have other negative side effects as well. @rbuckton
    • This is mitigated if the mutating decorator returns a class expression that extends from the target, or returns a proxy for the decorator. @rbuckton
  • Metadata for a method is attached to the class (or prototype) via the property key. It would not then be available if trying to read metadata on the function of the method (e.g. "tearing-off" the method from the class). @rbuckton

More Repositories

1

proposal-enum

Proposal for ECMAScript enums
HTML
214
star
2

prex

Async coordination primitives and extensions on top of ES6 Promises
TypeScript
149
star
3

grammarkdown

Markdown-like DSL for defining grammatical syntax for programming languages.
HTML
118
star
4

iterable-query

Query API over JavaScript (ECMAScript) Iterators
TypeScript
66
star
5

proposal-shorthand-improvements

A proposal to introduce new shorthand assignment forms for ECMAScript object literals
HTML
64
star
6

obs-remote

A touch-friendly desktop application designed for tablets that can be used to control OBS Studio remotely using 'obs-websocket'
TypeScript
36
star
7

proposal-functional-operators

Proposal for the additon of functional operators to ECMAScript
HTML
23
star
8

proposal-regexp-features

Proposal to investigate additional language features for ECMAScript Regular Expressions
JavaScript
19
star
9

asyncjs

Asynchronous coordination primatives for TypeScript and JavaScript
JavaScript
13
star
10

proposal-statements-as-expressions

Proposal to explore Statements as Expressions
JavaScript
12
star
11

proposal-struct

Value Types (i.e., 'struct') for ECMAScript
JavaScript
12
star
12

regexp-features

A comparison of Regular Expression features in various languages and libraries.
JavaScript
9
star
13

tsserver-live-reload

VSCode Extension to automatically restart the TypeScript Language Server when it changes.
TypeScript
8
star
14

graphmodel

JavaScript library for modeling directed graphs
TypeScript
7
star
15

sourcemap-visualizer

Source Map parser and visualizer
TypeScript
6
star
16

service-composition

Decorator-based dependency injection library.
TypeScript
5
star
17

iterable-query-linq

LINQ-like syntax via ECMAScript tagged templates
TypeScript
5
star
18

regexp-match-indices

Polyfill for the RegExp Match Indices proposal
TypeScript
4
star
19

posh-vsdev

Configures PowerShell to run as a Visual Studio Developer Command Prompt
PowerShell
4
star
20

promisejs

Promise/Future-based asynchrony in javascript
JavaScript
3
star
21

equatable

A low-level API for defining equality.
JavaScript
3
star
22

typedoc-plugin-linkrewriter

A TypeDoc plugin for rewriting links in markdown
TypeScript
2
star
23

chardata

Unicode character data
TypeScript
2
star
24

ecmarkup-vscode

ecmarkup language extensions for Visual Studio Code
HTML
2
star
25

fork-pipe

Fork/join stream processing.
TypeScript
2
star
26

chardata-generator

Generates TypeScript source files from the Unicode Character Database
TypeScript
1
star
27

typedoc-plugin-biblio

A TypeDoc plugin to support references to externally hosted documentation.
TypeScript
1
star
28

grammarkdown-server

Grammarkdown Language Server for VSCode
TypeScript
1
star
29

collection-core

Symbol-based augmentation API for interacting with collection-types in ECMAScript.
JavaScript
1
star
30

decorators

Annotations+Decorators TC39 proposal
HTML
1
star
31

rbuckton.github.io

1
star
32

grammarkdown-syntax

Grammarkdown Language Client for VSCode
JavaScript
1
star
33

proposal-regexp-conditionals

JavaScript
1
star
34

ecmascript-mirrors

Prototype and Proposal for a Mirrors API for ECMAScript, for use in tandem with Decorators.
TypeScript
1
star