Skip to content

vladima/ReflectDecorators

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Proposal to add Decorators to ES7, along with a prototype for an ES7 Reflection API for Decorator Metadata

Conditional code generation:

class Debug {  
    @conditional("debug")  
    static assert(condition: boolean, message?: string): void;  
}  
  
Debug.assert(false); // if window.debug is not defined Debug.assert is replaced by an empty function

Consider the Ember.js alias-like definition:

class Person {  
    constructor(public firstName: string, public lastName: string) { }
  
    @computed('firstName', 'lastName', (f, l) => l + ', ' + f)  
    fullName: string;
}  
  
var david = new Person('David', 'Tang');  
david.fullName; /// Tang, David

Consider Angular 2.0 DI implementation example:

class Engine {  
}  
  
class Car {  
    constructor(@Inject(Engine) engine: Engine) {}  
}  
  
var inj = new Injector([Car, Engine]);  
  
// AtScript compilation step adds a property “annotations” on Car of value [ new Inject(Engine) ].  
// At runtime, a call to inj.get would cause Angular to look for annotations, and try to satisfy dependencies.  
// in this case first by creating a new instance of Engine if it does not exist, and use it as a parameter to Car’s constructor  
var car = inj.get(Car);

Metadata that can be queried at runtime, for example:

class Fixture {  
    @isTestable(true)  
    getValue(a: number): string {  
        return a.toString();  
    }  
}  
  
// Desired JS  
class Fixture {  
    getValue(a) {  
        return a.toString();  
    }  
}  
Fixture.prototype.getValue.meta.isTestable = true;  
  
// later on query the meta data  
function isTestableFunction(func) {  
    return !!(func && func.meta && func.meta.isTestable);  
}

An extensible way to declare properties on or associate special behavior to declarations; design time tools can leverage these associations to produce errors or produce documentation. For example:

Deprecated, to support warning on use of specific API’s:

interface JQuery {  
    /**  
     * A selector representing selector passed to jQuery(), if any, when creating the original set.  
     * version deprecated: 1.7, removed: 1.9  
     */  
    @deprecated("Property is only maintained to the extent needed for supporting .live() in the jQuery Migrate plugin. It may be removed without notice in a future version.", false)  
    selector: string;  
}

Suppress linter warning:

@suppressWarning("disallow-leading-underscore")   
function __init() {  
}

A decorator is an expression that is evaluated after a class has been defined, that can be used to annotate or modify the class in some fashion. This expression must evaluate to a function, which is executed by the runtime to apply the decoration.

@decoratorExpression
class C {
}

A class decorator function is a function that accepts a constructor function as its argument, and returns either undefined, the provided constructor function, or a new constructor function. Returning undefined is equivalent to returning the provided constructor function.

// A class decorator
function dec(target) {  
   // modify, annotate, or replace target...
}

A property decorator function is a function that accepts three arguments: The object that owns the property, the key for the property (a string or a symbol), and optionally the property descriptor of the property. The function must return either undefined, the provided property descriptor, or a new property descriptor. Returning undefined is equivalent to returning the provided property descriptor.

// A property (or method/accessor) decorator
function dec(target, key, descriptor) {
	// annotate the target and key; or modify or replace the descriptor...
}

A parameter decorator function is a function that accepts two arguments: The function that contains the decorated parameter, and the ordinal index of the parameter. The return value of this decorator is ignored.

// A parameter decorator
function dec(target, paramIndex) {
	// annotate the target and index
}

A decorator factory is a function that can accept any number of arguments, and must return one of the above types of decorator function.

// a class decorator factory
function dec(x, y) {
	// the class decorator function
	return function (target) {
		// modify, annotate, or replace target...
	}
}

A decorator can be legally applied to any of the following:

  • A class declaration
  • A class property declaration (static or prototype)
  • A class method declaration (static or prototype)
  • A class get or set accessor declaration (static or prototype)
  • A parameter of a class constructor
  • A parameter of a class method (static or prototype)
  • A parameter of a class get or set accessor (static or prototype)

Please note that a decorator currently cannot be legally applied to any of the following:

  • A class constructor - This is to reduce ambiguity between where you can apply a decorator (on the class or on its constructor) and which of the above decorator function forms is called.
  • A function declaration - Decorators on a function declaration would introduce a TDZ (Temporal Dead Zone), which would make the function unreachable until its declaration is executed. This could cause confusion as an undecorated function declaration is hoisted and can be used in a statement preceeding the declaration.
  • A function expression - This is to reduce confusion and maintain parity with disallowing decorators on a function declaration.
  • An arrow function - This is to reduce confusion and maintain parity with disallowing decorators on a function expression.

This list may change in the future.

Decorators are evaluated in the order they appear preceeding their target declaration, to preserve side-effects due to evaluation order. Decorators are applied to their target declaration in reverse order, starting with the decorator closest to the declaration. This behavior is specified to preserve the expected behavior of decorators without a declarative syntax.

@F
@G
class C {	
}

For example, the above listing could be approximately written without decorators in the following fashion:

C = F(G(C))

In the above example, the expression F is evaluated first, followed by the expression G. G is then called with the constructor function as its argument, followed by calling F with the result. The actual process of applying decorators is more complex than the above example however, though you may still imperatively apply decorators with a reflection API.

If a class declaration has decorators on both the class and any of its members or parameters, the decorators are applied using the following pseudocode:

for each member M of class C
	if M is an accessor then
		let accessor = first accessor (get or set, in declaration order) of M
		let memberDecorators = decorators of accessor
		for each parameter of accessor
			let paramDecorators = decorators of parameter			
			let paramIndex = ordinal index of parameter
			Reflect.decorate(paramDecorators, accessor, paramIndex)
		next parameter

		let accessor = second accessor (get or set, in declaration order) of M
		if accessor then
			let memberDecorators = memberDecorators + decorators of accessor
			for each parameter of accessor
				let paramDecorators = decorators of parameter			
				let paramIndex = ordinal index of parameter
				Reflect.decorate(paramDecorators, accessor, paramIndex)
			next parameter
		end if
	else if M is a method
		let memberDecorators = decorators of M
		for each parameter of M
			let paramDecorators = decorators of parameter			
			let paramIndex = ordinal index of parameter
			Reflect.decorate(paramDecorators, M, paramIndex)
		next parameter
	else
		let memberDecorators = decorators of M
	end if

	let name = name of M
	let target = C.prototype if M is on the prototype; otherwise, C if M is static	
	Reflect.decorate(memberDecorators, C, name)
next member

for each parameter of C
	let paramDecorators = decorators of parameter
	let paramIndex = ordinal index of parameter
	Reflect.decorate(paramDecorators, C, paramIndex)
next parameter

let classDecorators = decorators of C
let C = Reflect.decorate(classDecorators, C)

The following are examples of how decorators can be desugared to ES6 (through a transpiler such as TypeScript). These examples levarage an imperative reflection API.

@F("color")  
@G  
class C {
}
var C = (function () {  
    class C {  
    }

    C = Reflect.decorate([F("color"), G], C);
    return C;
})();
class C {  
    @F("color")  
    @G  
    bar() { }  
}
var C = (function () {  
    class C {  
        bar() { }  
    }

    Reflect.decorate([F("color"), G], C.prototype, "bar");
    return C;  
})();
class C {  
    @F("color")  
    get bar() { }  

    @G  
    set bar(value) { }  
}
var C = (function () {  
    class C {  
        get bar() { }  
        set bar(value) { }  
    }  

    Reflect.decorate([F("color"), G], C.prototype, "bar");
    return C;  
})();
class C {  
    @F("color") 
    @g
    property;
}
var C = (function () {  
    class C {  
    }  

    Reflect.decorate([F("color"), G], C.prototype, "property");
    return C;  
})();
class C {  
    constructor(@F("color") @G x) {
    }
}
var C = (function () {  
    class C {  
        constructor(x) {  
        }  
    }  
  
  	Reflect.decorate([F("color"), G], C, /*paramIndex*/ 0);
    return C;  
})();
class C {  
    method(@F("color") @G x) {
    }
}
var C = (function () {  
    class C {  
        method(x) {  
        }  
    }  
  
  	Reflect.decorate([F("color"), G], C.prototype.method, /*paramIndex*/ 0);
    return C;  
})();
class C {  
    set accessor(@F("color") @G x) {
    }
}
var C = (function () {  
    class C {  
        set accessor(x) {  
        }  
    }  
  
  	Reflect.decorate([F("color"), G], Object.getOwnPropertyDescriptor(C.prototype, "accessor").set, /*paramIndex*/ 0);
    return C;  
})();

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 shim for this API can be found here: https://github.com/rbuckton/ReflectDecorators

module Reflect {
    /** Applies a set of decorators to a target object, property, or parameter. */
    export function decorate(decorators: Function[], target: Object, targetKeyOrIndex?: string | symbol | number): Function;

    /** A default metadata decorator factory that can be used on a class, class member, or parameter. */
    export function metadata(metadataKey: any, metadataValue: any): Function;
    
    /** Define a unique metadata entry on a target object, property, or parameter. */
    export function defineMetadata(metadataKey: any, metadata: any, target: Object, targetKeyOrIndex?: string | symbol | number): void;
    
    /** Gets a value indicating whether the provided metadata key exists in the prototype chain of a target object, property, or parameter. */
    export function hasMetadata(metadataKey: any, target: Object, targetKeyOrIndex?: string | symbol | number): boolean;

    /** Gets a value indicating whether the provided metadata key exists on a target object, property, or parameter. */
    export function hasOwnMetadata(metadataKey: any, target: Object, targetKeyOrIndex?: string | symbol | number): boolean;

    /** Gets the metadata value for the provided metadata key in the prototype chain of a target object, property, or parameter. */
    export function getMetadata(metadataKey: any, target: Object, targetKeyOrIndex?: string | symbol | number): any;

    /** Gets the metadata value for the provided metadata key on a target object, property, or parameter. */
    export function getOwnMetadata(metadataKey: any, target: Object, targetKeyOrIndex?: string | symbol | number): any;

    /** Gets the metadata keys in the prototype chain of a target object, property, or parameter. */
    export function getMetadataKeys(target: Object, targetKeyOrIndex?: string | symbol | number): any[];

    /** Gets the metadata keys of a target object, property, or parameter. */
    export function getOwnMetadataKeys(target: Object, targetKeyOrIndex?: string | symbol | number): any[];

    /** Deletes a metadata entry on a target object, property, or parameter. */
    export function deleteMetadata(metadataKey: any, target: Object, targetKeyOrIndex?: string | symbol | number): boolean;

    /** Merges unique metadata from a source into a target, returning the target. */
    export function mergeMetadata(target: Object, source: Object): Object;
}
// An "annotation" factory for a class
function Component(options) {
  return target => Reflect.defineMetadata("component", options, target);
}

// 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
function MarshalAs(options) {
  return (target, propertyKey) => Reflect.defineMetadata(MarshalAs, options, target, propertyKey);
}

// 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
function Inject(type) {
  return (target, parameterIndex) => Reflect.defineMetadata(Inject, type, target, parameterIndex);
}

// NOTE: A "decorator" factory for a parameter cannot mutate the parameter.
@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;
  }
}
class MyComponent extends ComponentBase {
  constructor(myService) {
    this.myService = myService;
  }
  
  get service() {
    return this.myService;
  }
}

Reflect.decorate([MarshalAs({ /*options...*/}), Enumerable(true)], MyComponent.prototype, "service");
Reflect.decorate([Inject(ServiceBase)], MyComponent, 0);
MyComponent = Reflect.decorate([Component({ /*options...*/ }), Logged("called class")], MyComponent);
// 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.getMetadata(Inject, componentType, i);
      if (injectType) {
        args[i] = this.get(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);

NOTE: this section is out of date and will soon be updated.

  DecoratorList [Yield] :
   DecoratorList [?Yield]optDecorator [?Yield]

  Decorator [Yield] :
   @LeftHandSideExpression [?Yield]

  PropertyDefinition [Yield] :
   IdentifierReference [?Yield]
   CoverInitializedName [?Yield]
   PropertyName [?Yield]:AssignmentExpression [In, ?Yield]
   DecoratorList [?Yield]optMethodDefinition [?Yield]

  CoverMemberExpressionSquareBracketsAndComputedPropertyName [Yield] :
   [Expression [In, ?Yield]]

NOTE The production CoverMemberExpressionSquareBracketsAndComputedPropertyName is used to cover parsing a MemberExpression that is part of a Decorator inside of an ObjectLiteral or ClassBody, to avoid lookahead when parsing a decorator against a ComputedPropertyName.

  PropertyName [Yield, GeneratorParameter] :
   LiteralPropertyName
   [+GeneratorParameter] CoverMemberExpressionSquareBracketsAndComputedPropertyName
   [~GeneratorParameter] CoverMemberExpressionSquareBracketsAndComputedPropertyName [?Yield]

  MemberExpression [Yield]  :
   [Lexical goal InputElementRegExp] PrimaryExpression [?Yield]
   MemberExpression [?Yield]CoverMemberExpressionSquareBracketsAndComputedPropertyName [?Yield]
   MemberExpression [?Yield].IdentifierName
   MemberExpression [?Yield]TemplateLiteral [?Yield]
   SuperProperty [?Yield]
   NewSuperArguments [?Yield]
   newMemberExpression [?Yield]Arguments [?Yield]

  SuperProperty [Yield] :
   superCoverMemberExpressionSquareBracketsAndComputedPropertyName [?Yield]
   super.IdentifierName

  CallExpression [Yield] :
   MemberExpression [?Yield]Arguments [?Yield]
   SuperCall [?Yield]
   CallExpression [?Yield]Arguments [?Yield]
   CallExpression [?Yield]CoverMemberExpressionSquareBracketsAndComputedPropertyName [In, ?Yield]
   CallExpression [?Yield].IdentifierName
   CallExpression [?Yield]TemplateLiteral [?Yield]

  FormalRestParameter [Yield] :
   DecoratorList [?Yield]optBindingRestElement [?Yield]

  FormalParameter [Yield, GeneratorParameter] :
   DecoratorList [?Yield]optBindingElement [?Yield, ?GeneratorParameter]

  ClassDeclaration [Yield, Default] :
   DecoratorList [?Yield]optclassBindingIdentifier [?Yield]ClassTail [?Yield]
   [+Default] DecoratorList [?Yield]optclassClassTail [?Yield]

  ClassExpression [Yield, GeneratorParameter] :
   DecoratorList [?Yield]optclassBindingIdentifier [?Yield]optClassTail [?Yield, ?GeneratorParameter]

  ClassElement [Yield] :
   DecoratorList [?Yield]optMethodDefinition [?Yield]
   DecoratorList [?Yield]optstaticMethodDefinition [?Yield]

  ExportDeclaration :
   export*FromClause;
   exportExportClauseFromClause;
   exportExportClause;
   exportVariableStatement
   exportLexicalDeclaration
   exportHoistableDeclaration
   DecoratorList optexport [lookahead ≠ @] ClassDeclaration
   exportdefaultHoistableDeclaration [Default]
   DecoratorList optexportdefault [lookahead ≠ @] ClassDeclaration [Default]
   exportdefault [lookahead  { function, class, @ }] AssignmentExpression [In];

interface TypedPropertyDescriptor<T> {  
    enumerable?: boolean;  
    configurable?: boolean;  
    writable?: boolean;  
    value?: T;  
    get?: () => T;  
    set?: (value: T) => void;  
}  
  
interface ClassDecorator<TFunction extends Function> {  
    (target: TFunction): TFunction | void;  
}  
  
interface ParameterDecorator {  
    (target: Function, parameterIndex: number): void;  
}  
  
interface PropertyDecorator<T> {  
    (target: Object, propertyKey: PropertyKey, descriptor: TypedPropertyDescriptor<T>): TypedPropertyDescriptor<T> | void;  
}  

TypeScript compiler can add additional type information then a declaration includes decorators. The types provided are in a serialized form. Serialization logic is descriped in C.2. Reading this type information requires the use of a reflection API (or polyfill for ES6).

@dec
class C {  
    constructor(a: Object, b: number, c: { a: number }, d: C2) {  
    }

    @dec
    property: string;    

    @dec
    method(): boolean {
    	return true;
    }
}  
  
function dec(target: Object, keyOrIndex?: string | symbol | number): void {
	var type = Reflect.getMetadata("design:type", target, keyOrIndex);
	var paramTypes = Reflect.getMetadata("design:paramtypes", target, keyOrIndex);    
	var returnType = Reflect.getMetadata("design:returntype", target, keyOrIndex);
}

// ES7 emit
@dec
@Reflect.metadata("design:type", Function)
@Reflect.metadata("design:paramtypes", [Object, Number, Object, C2 || Object])
class C {  
    constructor(a: Object, b: number, c: { a: number }, d: C2) {  
    }  

    @dec
    @Reflect.metadata("design:type", String)
    property; // assumes property declarations in ES7

    @dec
    @Reflect.metadata("design:type", Function)
    @Reflect.metadata("design:paramtypes", [])
    @Reflect.metadata("design:returntype", Boolean)
    method() {
    }
}  

// ES6 emit
var __decorate = this.__decorate || (typeof Reflect === "object" && Reflect.decorate) || function (decorators, target, key) {
    var kind = key == null ? 0 : typeof key === "number" ? 1 : 2, result = target;
    if (kind == 2) result = Object.getOwnPropertyDescriptor(target, typeof key === "symbol" ? key : key = String(key));
    for (var i = decorators.length - 1; i >= 0; --i) {
        var decorator = decorators[i];
        result = (kind == 0 ? decorator(result) : kind == 1 ? decorator(target, key) : decorator(target, key, result)) || result;
    }
    if (kind === 2 && result) Object.defineProperty(target, key, result);
    if (kind === 0) return result;
};
var __metadata = this.__metadata || (typeof Reflect === "object" && Reflect.metadata) || function () { return function () { }; };

var C = (function() {
	class C {  
	    constructor(a, b, c, d) {  
	    }
	    method() {	    
		}
	}
	__decorate([dec, __metadata("design:type", String)], C.prototype, "property");
	__decorate([dec, __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Boolean)], C.prototype, "method");
	C = __decorate([dec, __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Number, Object, C2 || Object])], C);
	return C;  
})();
class C { }  
interface I { }  
enum E { }  
module M { }

Formal parameter list in a call signature like so:

(a: number, b: boolean, c: C, i: I, e: E, m: typeof M, f: () => void, o: { a: number; b: string; })

Serializes as:

[Number, Boolean, C, Object, Number, Object, Function, Object]
  • number serialized as Number
  • string serialized as String
  • boolean serialized as Boolean
  • any serialized as Object
  • void serializes as undefined
  • Array serialized as Array
  • If a Tuple, serialize as Array
  • If a class serialize it as the class constructor
  • If an Enum serialize it as Number
  • If has at least one call signature, serialize as Function
  • Otherwise serialize as Object

Some applications may need a way to easily inject type information in a fashion similar to TypeScript's mechanism, though the applications themselves are written using regular JavaScript. A library could choose to make this process easier for these applications by exposing wrapper metadata functions:

// [annotations.ts]
export function Type(type: Function): Decorator {
	return Reflect.metadata("design:type", type);
}

export function ParamTypes(...types: Function[]): Decorator {
	return Reflect.metadata("design:paramtypes", types);
}

export function ReturnType(type: Function): Decorator {
	return Reflect.metadata("design:returntype", type);
}

// app.js
define(["exports", "annotations"], function (exports, annotations) {
	var Component = annotations.Component;
	var Type = annotations.Type;
	var ParamTypes = annotations.ParamTypes;
	var ReturnType = annotations.ReturnType;

	function MyComponent(a, b) {
	}
	
	MyComponent = Reflect.decorate([Component({ ... }), Type(Function), ParamTypes([UserServiceBase, LocationServiceBase])], MyComponent);
	exports.MyComponent = MyComponent;
})

TypeScript would not be providing these helpers, it would be up to library authors to add these if they determine they are necessary.

  • Do we want to enable more elaborate serialization?
    • Serialize interfaces or type literals? For example, serialize the type literal { a: string; b: number } as { a: String, b: Number } instead of just Object.
    • Serialize generic type references? One suggestion was to serialize Array<Number> as [Array, Number]
    • Serialize tuple types?
    • Serialize union types?

About

Prototype for an ES7 Reflection API for Decorator Metadata

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • HTML 46.4%
  • TypeScript 32.3%
  • JavaScript 18.5%
  • CSS 2.8%