Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active April 13, 2018 08:37
Show Gist options
  • Save rbuckton/de6230d3523c3a59c443 to your computer and use it in GitHub Desktop.
Save rbuckton/de6230d3523c3a59c443 to your computer and use it in GitHub Desktop.
Unification of Decorators and Annotations for ES7

Summary

One issue introduced by decorators is that any attempt to use them on a function declaration would either necessitate a TDZ for the declaration, or would need to be an error. This limits the usefullness of decorators.

Another issue is that many use cases for annotations need a coherent runtime API that allows for imperative reflection for annotations of an object, its members, or the parameters of a function.

This document describes two parts to a solution for unifying decorators and annotations:

  1. Marking decorators as "early" with the @decorator decorator.
  2. Extending the Reflect API with a comprehensive set of functions for reflecting and managing metadata.

The @decorator decorator

@decorator({ early: true })
declare function decorator(options?: { early?: boolean; }): Function;

A general-purpose decorator can only be executed at the point in the source where the declaration is defined, as it introduces an expression context into a declaration in a fashion similar to the ClassHeritage clause of a class declaration. This is because a general-purpose decorator may have side-effects that could affect execution order, and as a result introduces a TDZ. For class declarations, class expressions, or function expressions this is not an issue. For a function declaration, however, applying a general-purpose decorator would change the runtime semantics of the declaration.

To resolve this issue, we propose the introduction of a special @decorator decorator with special static semantic meaning. A function declaration can be decorated with @decorator, and can provide options that affect that function declaration's usage as a decorator. If the early property is set to true, the static and runtime semantics of the decorator change.

An early decorator has special static semantics:

  1. It can only be referenced in a decorator expression by name, by itself or with a single trailing argument list.
  2. Arguments to an early decorator must be constant values.
  3. All early decorators are separated from non-early decorators and applied as part of the declaration of the function.

For purposes of the above steps, constant values are defined as:

  • String literals
  • Numeric literals
  • Boolean literals
  • Regular Expression literals
  • null
  • pure expressions that can be reduced to a constant value.
  • Object literals consisting of SingleNameBinding keys only, and whose values are constant values.
  • Array literals consisting of constant values only.
  • Function expressions.
  • Arrow functions with no this binding.
  • Identifiers bound to a function declaration1 2.
  • Identifiers bound to a const declaration whose initializer consists only of constant values.
  • Identifiers bound to an import of an exported function declaration1 2.
  • Identifiers bound to an import of an exported const declaration whose initializer consists only of constant values.

Notes:

  1. The value used during runtime execution is the value of the function declaration after any early decorators are applied. It would be an error to reference a function declaration with any non-early decorators.
  2. Any circular reference between early decorators is a TypeError (excluding the @decorator decorator itself).

Metadata Reflection

In addition to a declarative approach to defining decorators, it is necessary to also include an imperative API capable of applying decorators, as well as defining, reflecting over, and removing decorator metadata from an object, property, or parameter.

A rough shim for this API can be found here: https://gist.github.com/rbuckton/5903ff8a617f6afc5797

API

declare var Reflect: {
    // decorators
    decorate(target: Function, decorators: Function[], options: { metadata?: { overwrite?: boolean; properties?: boolean; parameters?: boolean; }): Function;
    decorateProperty(target: any, propertyKey: PropertyKey, decorators: Function[]): any;
    decorateParameter(target: Function, parameterIndex: number, decorators: Function[]): void;
    
    // object metadata
    defineMetadata(target: any, metadataKey: any, metadata: any): void;
    hasMetadata(target: any, metadataKey: any): boolean;
    getMetadata(target: any, metadataKey: any): any;
    getMetadataKeys(target: any): any[];
    hasOwnMetadata(target: any, metadataKey: any): boolean;
    getOwnMetadata(target: any, metadataKey: any): any;
    getOwnMetadataKeys(target: any): any[];
    deleteOwnMetadata(target: any, metadataKey: any): boolean;
    copyOwnMetadata(target: any, source: any, options?: { overwrite?: boolean; properties?: boolean; parameters?: boolean; }): void;
    
    // property metadata
    definePropertyMetadata(target: any, propertyKey: PropertyKey, metadataKey: any, metadata: any): void;
    hasPropertyMetadata(target: any, propertyKey: PropertyKey, metadataKey: any): boolean;
    getPropertyMetadata(target: any, propertyKey: PropertyKey, metadataKey: any): any;
    getPropertyMetadataKeys(target: any, propertyKey: PropertyKey): any[];
    hasOwnPropertyMetadata(target: any, propertyKey: PropertyKey, metadataKey: any): boolean;
    getOwnPropertyMetadata(target: any, propertyKey: PropertyKey, metadataKey: any): any;
    getOwnPropertyMetadataKeys(target: any, propertyKey: PropertyKey): any[];    
    deleteOwnPropertyMetadata(target: any, propertyKey: PropertyKey, metadataKey: any): boolean;
    copyOwnPropertyMetadata(target: any, source: any, propertyKey: propertyKey, options?: { overwrite?: boolean; }): void;
    
    // parameter metadata
    defineParameterMetadata(target: Function, parameterIndex: number, metadataKey: any, metadata: any): void;
    hasParameterMetadata(target: Function, parameterIndex: number, metadataKey: any): boolean;
    getParameterMetadata(target: Function, parameterIndex: number, metadataKey: any): any;
    getParameterMetadataKeys(target: Function, parameterIndex: number): any[];
    deleteParameterMetadata(target: Function, parameterIndex: number, metadataKey: any): boolean;
    copyParameterMetadata(target: any, source: any, parameterIndex: number, options?: { overwrite?: boolean; }): void;
}

Examples

// An "annotation" factory for a function/class
@decorator({ early: true }) 
function Component(options) {
  return target => Reflect.defineMetadata(target, Component, options);
}

// A "decorator" factory that replaces the function/class with a proxy
function Logged(message) {
  return target => new Proxy(target, {
    apply(target, thisArg, argArray) {
      console.log(message);
      return Reflect.apply(target, thisArg, argArray);
    },
    construct(target, thisArg, argArray) {
      console.log(message);
      return Reflect.construct(target, argArray);
    }
  });
}

// An "annotation" factory for a member
@decorator({ early: true }) 
function MarshalAs(options) {
  return (target, propertyKey) => Reflect.definePropertyMetadata(target, propertyKey, MarshalAs, options);
}

// A "decorator" factory for a member that mutates its descriptor
function Enumerable(value) {
  return (target, propertyKey, descriptor) => {
    descriptor.enumerable = value;
    return descriptor;
  };
}

// An "annotation" factory for a parameter
@decorator({ early: true }) 
function Inject(type) {
  return (target, parameterIndex) => Reflect.defineParameterMetadata(target, parameterIndex, Inject, type);
}

// A "decorator" factory for a parameter cannot mutate the parameter.

Declarative Usage

@Component({ /*options...*/ })
@Logged("Called class")
class MyComponent extends ComponentBase {
  constructor(@Inject(ServiceBase) myService) {
    this.myService = myService;
  }
  
  @MarshalAs({ /*options...*/ })
  @Enumerable(true)
  get service() {
    return this.myService;
  }
}

Imperative Usage

class MyComponent extends ComponentBase {
  constructor(myService) {
    this.myService = myService;
  }
  
  get service() {
    return this.myService;
  }
}

Reflect.decorateProperty(MyComponent.prototype, "service", [MarshalAs({ /*options...*/}), Enumerable(true)]);
Reflect.decorateParameter(MyComponent, 0, [Inject(ServiceBase)]);
MyComponent = Reflect.decorate(MyComponent, [Component({ /*options...*/ }), Logged("called class")]);

Composition Sample

// read annotations
class Composer {
  constructor() {
    this.types = new Map();
    this.components = new Map();
  }
  for(baseType) {
    return { use: (componentType) => this.types.set(baseType, componentType) };
  }
  get(type) {
    if (this.components.has(type)) {
      return this.components.get(type);
    }
    let componentType = type;
    if (this.types.has(type)) {
      componentType = this.types.get(type);
    }
    let args = new Array(componentType.length);
    for (let i = 0; i < args.length; i++) {
      let injectType = Reflect.getParameterMetadata(componentType, i, Inject);
      if (injectType) {
        args[i] = getComponent(injectType);
      }
    }
    let component = Reflect.construct(componentType, args);
    this.components.set(type, component);
    return component;
  }
}

let composer = new Composer();
composer.for(ServiceBase).use(MyService);
composer.for(ComponentBase).use(MyComponent);
let component = composer.get(ComponentBase);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment