microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.3k stars 12.39k forks source link

Decorators #2249

Closed rbuckton closed 8 years ago

rbuckton commented 9 years ago

ES7 proposal

The ES7 proposal for decorators can be found here: https://github.com/wycats/javascript-decorators The ES7 proposal serves as the base of this proposal. Below are notes about how the type system

Decorator targets:

Class constructor

@F("color")
@G
class Foo {
}

desugars to:

var Foo = (function () {
    function Foo() {
    }
    Foo = __decorate([F("color"), G], Foo);
    return Foo;
})();

Methods

class Foo {
  @F(color)
  @G
  bar() { }
}

desugars to:

var Foo = (function () {
    function Foo() {
    }
    Foo.prototype.bar = function () {
    };
    Object.defineProperty(Foo.prototype, "bar", __decorate([F(color), G], Foo.prototype, "bar", Object.getOwnPropertyDescriptor(Foo.prototype, "bar")));
    return Foo;
})();

Static method

class Foo {
    @F("color")
    @G
    static sMethod() {}
}

desugars to:

var Foo = (function () {
    function Foo() {
    }
    Foo.sMethod = function () {
    };
    Object.defineProperty(Foo, "sMethod", __decorate([F("color"), G], Foo, "sMethod", Object.getOwnPropertyDescriptor(Foo, "sMethod")));
    return Foo;
})();

Properties

class Foo {
    @F("color")
    @G
    prop: number;
}

desugars to:

var Foo = (function () {
    function Foo() {
    }
    __decorate([F("color"), G], Foo.prototype, "prop");
    return Foo;
})();

Method/Accessor formal parameter

class Foo {
    method(@G a, @F("color") b) {}
}

desugars to:

var Foo = (function () {
    function Foo() {
    }
    Foo.prototype.method = function (a, b) {
    };
    __decorate([G], Foo.prototype, "method", 0);
    __decorate([F("color")], Foo.prototype, "method", 1);
    return Foo;
})();

Where the __decorate is defined as:

var __decorate = this.__decorate || function (decorators, target, key, value) {
    var kind = typeof (arguments.length == 2 ? value = target : value);
    for (var i = decorators.length - 1; i >= 0; --i) {
        var decorator = decorators[i];
        switch (kind) {
            case "function": value = decorator(value) || value; break;
            case "number": decorator(target, key, value); break;
            case "undefined": decorator(target, key); break;
            case "object": value = decorator(target, key, value) || value; break;
        }
    }
    return value;
};

Decorator signatures:

A valid decorator should be:

  1. Assignable to one of the Decorator types (ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator) as described below.
  2. Return a value (in the case of class decorators and method decorator) that is assignable to the decorated value.
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Function, propertyKey: string | symbol, parameterIndex: number) => void;

Notes:

fdecampredon commented 9 years ago

Excuse me from what I understand of the spec, we won't be able to do:

@F
function test() {
}

Am I right ?

ivogabe commented 9 years ago

How does type serialization work with rest arguments?

@F()  
class Foo {  
    constructor(...args: string[]) {  
    }  
}  

function F(@paramterTypes types?: Function[]) {  
    return function (target) {  
        target.paramterTypes = types; // ???  
    }  
}
MgSam commented 9 years ago

Using decorators seems straightforward enough, but I found the sections about declaring them to be confusing. C.4 says decorators need to be annotated with @decorator, but not a single one of the examples actually shows this happening.

Are decorator factories intended to be classes that implement the interfaces found in B?

JsonFreeman commented 9 years ago

What is the rule for refining the interpretation of CoverMemberExpressionSquareBracketsAndComputedPropertyName?

JsonFreeman commented 9 years ago

I noticed many of the typings have Function | Object at various points, but these will degenerate to Object at type check time. What is the reason to have Function there?

JsonFreeman commented 9 years ago

I am not crazy about the terms DecoratorFunction vs DecoratorFactory. I'd much rather follow the nomenclature of generators, which has Generator and GeneratorFunction. With this scheme, we would rename DecoratorFunction to Decorator, and DecoratorFactory to DecoratorFunction.

JsonFreeman commented 9 years ago

For the decorated exports, what is [lookahead ≠ @] for? Can HoistableDeclaration and ClassDeclaration actually start with a @?

jayphelps commented 9 years ago

This is a dup of #1557

JsonFreeman commented 9 years ago

It's not really a dupe, as #1557 was for a different design. This issue is for the decorators design being implemented now.

jayphelps commented 9 years ago

My mistake.

fdecampredon commented 9 years ago

For decorator on function expression, could we not do something like :

@F("color") @G 
function myFunc() {
   doSomething();
}

transformed in :

var _t = function() {
   doSomething();
}
_t = F("color")(_t = G(_t) || _t) || _t;  

function myFunc() {
  return _t.apply(this, arguments)
}

It's a bit bother some to have to right every function like :

const myFunc = function () {}

You loose hoisting, and function.name

mhegazy commented 9 years ago

Implementation added in PR #2399

mhegazy commented 9 years ago

Updates: Proposal updated, and added link to the @wycats ES7 JavaScript decorators.

fdecampredon commented 9 years ago

saddened that it became a class only thing ... Also what about ambiant decorator did they get out of the scope ?

JsonFreeman commented 9 years ago

@fdecampredon with your proposal for functions, seems like you still lose the hoisting.

fdecampredon commented 9 years ago

@JsonFreeman why ? if you insert _t at the top of the file ?

import something from 'something';

myFunc(something.something());

@F("color") @G 
function myFunc() {
  doSomething()
}
import something from 'something';

var _t = function() {
   doSomething();
}
_t = F("color")(_t = G(_t) || _t) || _t;  

myFunc(something.something());

function myFunc() {
  return _t.apply(this, arguments)
}

Also even if my proposal has a lot of issues, I would seriously like to be able to use decorators on function (even if I have to use variable assigned function) and lose hoisting. Case like this gist seems a pretty good decorator use case for both function and class (especially coupled with ambient decorators if they still end up being in the scope)

mhegazy commented 9 years ago

@fdecampredon this does not work for the general case, as the decorators are expressions themselves. e.g.

myFunc();  // assumes function declaration is hoisted

var dec = (t) => t; // defininig a decorator

@dec
function myFunc() {}

if you hoist the function declaration and application of the decorator then you break the decorator. if you only hoist the function declaration, but not the decorator application you can witness the function in an undecorated state. no appealing solutions here.

this is the same issue as with class declaration extend clause, which in ES6 is an expression. the result was not hoisting the class declaration, just the symbol (akin to var declaration, but not function declarations)

fdecampredon commented 9 years ago

Oups didn't think about it thank you @mhegazy. However why the function part have completely abandoned the original @jonathandturner proposal had the rule :

decorated function declarations cannot be hoisted to the containing scope

Loosing hoisting is sure a drawback, but I find it damageable to transform it into an class only feature when it would have use case for other construct.

JsonFreeman commented 9 years ago

Let's see what the desired set of constraints seem to imply:

The only resolution I can see for this is the following: For function decorators, we only allow something of the form @identifier. We do not allow a left hand side expression. In addition, the identifier must be a direct reference to a function declaration (including a decorated function). All function decorations that take place in the scope must be emitted at the top of the scope, in the order that they were applied.

mhegazy commented 9 years ago

The problem breaking hoisting rules is that it is surprising. If you are writing javascript for a while you expect function declarations to be available before they are lexically declared; now by adding a seemingly simple syntactic marker (the decorator) this fundamental nature of function declaration is altered.

Having said that, the ES7 proposal is still in its initial stages, so I expect it to evolve and expand; so it is conceivable that the final proposal would include functions in some form.

JsonFreeman commented 9 years ago

I do think they should be hoisted. I am saying that the only decorations we allow on functions are decorations that are definitely themselves hoisted. Namely identifiers that reference function declarations.

But there is another problem here. It is actually impossible to simultaneously hoist all decorated functions and ensure that they are not observed in their undecorated state. The problem can be seen with a decorator cycle.

@dec1
function dec2(target: Function) {
   // Do stuff
}

@dec2
function dec1(target: Function) {
   // Do stuff
}

Even if both functions are hoisted, which one gets decorated first? If dec2 gets decorated first, then dec1 will not itself be decorated by the time it is applied to dec2.

So we would have to choose between the following:

While I don't like either of these, I don't even think anything else is possible.

mhegazy commented 9 years ago

This is the JS proposal. so the engine would not know if the expression refers to a function declaration or not, with static analysis we could tell though. consider this:

@dec1
function dec2(target: Function) {
   // Do stuff
}

dec2 = undefined;

@dec2
function dec1(target: Function) {
   // Do stuff
}
fletchsod-developer commented 9 years ago

Ugh! Javascript not getting any simplier anymore. :-)

ivogabe commented 9 years ago

It would be easier to enable it only on function expressions:

const foo = @decorator () => {
    // ...   
}
const bar = @decorator function() {
    // ...
}

Function expressions or lambda's aren't hoisted.

cybrown commented 9 years ago

Is it possible to have parameters like this in a decorator in typescript 1.5.0-alpha ?

@dec1({key1: value1, key2, value2})
function dec2(target: Function) {
   // Do stuff
}
cybrown commented 9 years ago

Ok nevermind, just create a factory which takes the parameters and return the actual decorator function.

For example, a class decorator with a string parameter:

function decoratorWithString(param: string) { // Decorator factory
    return function(target) { // Actual decorator
        // Do stuff with target and string parameter
    }
}

// Usage
@decoratorWithString('foobar')
class Foo {

}
cmichaelgraham commented 9 years ago

greetings.

i am trying to figure out how to write a decorator that will pick up the types declared in the constructor.

here's some code that illustrates what i'm trying to do, but it is hardwired. i need it to respond to the constructor declaration in class D


class A {
  public message = "identity: class A";
}

class B {
  public message = "identity: class B";
}

@decoTest
class D {
  static metadata:Array<Function> = [];
  constructor(a: A, b: B) {
  }
}
describe("decorators", function() {
  it("should inject constructor types", function() {
    var d = new D(new A(), new B());
    expect(D.metadata.length).toBe(2);
  });
});

function decoTest<T>(target: T, ...rest) {
  target["metadata"].push(A, B); // how do i get this based on constructor ???
  return target;
}
cybrown commented 9 years ago

I am afraid that type information is out of scope of the decorator feature. You can use ParameterDecorator on each parameter to add that kind of information.

cybrown commented 9 years ago

The typing or implementation for ParameterDecorator is not quite correct, from the typings the target is a function, but while using typescript, it is the prototype object. I must cast the target parameter in order to get the right type:

function MyParameterDecorator (_target: Function, methodName: string, index: number) {
    const target = <InterfaceForMyUseCase><anyt>_target;
    // do stuff
}

Instead of:

function MyParameterDecorator (target: InterfaceForMyUseCase, methodName: string, index: number) {
    // do stuff
}
cmichaelgraham commented 9 years ago

wow - parameter decorators rule !!!!!!!!!!!!!!! see this repo

here's the output from running the tests:

LOG: 'injectMe:'
LOG: '  type: class A'
LOG: '  type: class B'
LOG: '  some key'

and the code:

module ParameterDecorators {
  class A {
    static typeName:string = "type: class A";
    public instanceTypeName = "instance: class A";
  }

  class B {
    static typeName:string = "type: class B";
    public instanceTypeName = "instance: class B";
  }

  @injectTest(A, B, "some key")
  class C {
    static injectMe: Array<any> = [];
    constructor(a: A, b: B) {
    }
  }

  function injectTest(...rest) {
    return function(target): void {
      target["injectMe"] = rest;
    }
  }

  describe("decorators", function() {
    it("should inject dependency-injection keys", function() {
      var c = new C(new A(), new B());
      console.log("injectMe:");
      for (let parm of C.injectMe) {
        if (typeof(parm) === "function") {
          console.log("\t" + parm["typeName"]);
        } else {
          console.log("\t" + parm)
        }
      }
    });
  });
}
cybrown commented 9 years ago

I've made a wrapper around express (but any web framework could be supported, an adapter interface is defined) with a decorator based API: https://github.com/cybrown/web-decorators

I am using ClassDecorator, ParameterDecorator and MethodDecorator.

rbuckton commented 9 years ago

@cybrown I just updated the signature for ParameterDecorator in #2635, which is now in master.

cybrown commented 9 years ago

@rbuckton Thank you, I'll update that tomorow in my project.

cybrown commented 9 years ago

Is it possible to have the name of the parameter in a ParameterDecorator ? This may be useful because I think that the name of the parameter might be half of the time the first argument of a ParameterDecorator. Moreover, it might be a solution to parameter name mangling by minifiers.

cmichaelgraham commented 9 years ago

i've been asked to accomplish this:

@inject(A, B, "some key")
  class C {
    static injectMe: Array<any> = [];
    constructor(a: A, b: B) {
    }
  }

  function inject(...rest) {
    return function(target): void {
      target["inject"] = rest;
    }
  }

but without specifying the keys in the inject decorator, but instead having the decorators pick up the constructor's class functions automatically with something along these lines:

inject(@parameterTypes types:Function[]){ ... }

i'm looking for how to do what @jonathandturner describes here

cybrown commented 9 years ago

@cmichaelgraham Yes having runtime type information (rtti as described in what was AtScript) would be awesome for that kind of usage, even in a completely separated feature such as introspection.

basarat commented 9 years ago

@cmichaelgraham look at https://github.com/Microsoft/TypeScript/pull/2589

rbuckton commented 9 years ago

@cmichaelgraham We are working on a proposal for ES7 to add a metadata API that will work together with decorators. #2589 adds experimental support for that proposal, but requires a separate polyfill be present to read the metadata.

EisenbergEffect commented 9 years ago

@rbuckton @cmichaelgraham There was originally a design for special a TypeScript decorator that would tell the compiler to inject types into the decorator, based on the target being decorated. I think it was @parameterTypes Am I correct in thinking that that feature is being removed in favor of the more general metadata api? If so, I would support that.

Can you talk a bit about the status of that with respect to TypeScript. Is that planned for the release of 1.5? If so, what will the compiler options look like? One thing that would be useful is to have the compiler automatically generate the type metadata for constructor signatures only.

mhegazy commented 9 years ago

@EisenbergEffect Decorators are part of the 1.5, you can start using it with our latest release 1.5-alpha.

As for meta data support. The desing has changed since the orignal proposal for @paramtypes. the new desing is using Reflect.metada proposal. See #2589 for more details on how it works. also @rbuckton has a pollyfill to consume the metadata here: https://github.com/rbuckton/reflectDecorators

EisenbergEffect commented 9 years ago

@mhegazy I know that. See here: http://blog.durandal.io/2015/04/09/aurelia-update-with-decorators-ie9-and-more/ :smile:

I'm also familiar with the metadata proposal. I've been providing feedback on that. I just wanted to know if the original @parameterTypes idea was being removed.

mhegazy commented 9 years ago

thanks @EisenbergEffect for sharing. :+1:

Yes. the only issue with @paramtypes is it makes emit directed by the type system, and requiring global program information. This breaks scenarios for single-module transpilation (see #2499). The other option was to put it on call sites, which adds a lot of work on the decorator users instead of authors. So we went back to the drawing board and landed on the Reflect.metadata approach instead.

mhegazy commented 9 years ago

if you also looked at earlier versions of the proposal, we had to remove the ambient/design-time decorators for the same reason.

EisenbergEffect commented 9 years ago

Thanks for clarifying. That all makes sense. Any idea if the Reflect-based approach will land in 1.5?

mhegazy commented 9 years ago

yes. it is currently in master. it should be available in the next release. it is currently an opt-in feature using the experimental flag --emitDecoratorMetadata; and it only adds metadata to decorated entities.

EisenbergEffect commented 9 years ago

"it only adds metadata to decorated entities" Can you expand on that idea? Is a special decorator involved? Or does it add it to anything with any decorator? In other words, if a developer uses Aurelia's inject decorator, then will that trigger the compiler to generate the metadata?

mhegazy commented 9 years ago

if a developer uses Aurelia's inject decorator, then will that trigger the compiler to generate the metadata?

yes.

what i meant is:

@inject
class Foo {
    constructor(a: number, b: string) {}
}

class Bar {
    constructor(a: number, b: string) {}
}

with --emitDecoratorMetadata the compiler will emit type metadata (i.e. call to reflect.metadata('desing:paramtypes', [Number, String])) for Foo but not Bar.

EisenbergEffect commented 9 years ago

That will work for us. We'll prepare support for the Reflect.metadata api for our next release, most likely next week. Thanks for the clarification!

cmichaelgraham commented 9 years ago

so what is emitted for this?

@inject
class Foo {
    constructor(a: A, b: B) {}
}

class Bar {
    constructor(a: number, b: B) {}
}

would it be (for Foo):

reflect.metadata('desing:paramtypes', [A, B])
rbuckton commented 9 years ago

@cmichaelgraham Yes, that is roughly what is generated.