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:

qc00 commented 9 years ago

It seems it is not currently possible to decorate parameter properties. Can they be supported?

qc00 commented 9 years ago

Parameter properties are thing like constructor(private prop: Type), where a property (field) and a parameter is declared together. The problem is both of them can be decoreated, so some imaginative syntax may have to be invented.

loucyx commented 9 years ago

Question: Can the decorators be used to modify the behavior of a plain object method, like...

var isCreatorUser = () => {  /* code that verifies if user is the creator */ },
    isAdminUser = () => { /* code that verifies if user is admin */ },

var routeConfig = {
    get() {},
    @isCreatorUser patch() {},
    @isAdminUser @isCreatorUser delete() {}
};

... and if not, why? It will be really useful.

JsonFreeman commented 9 years ago

Not yet, but we are considering it. @rbuckton might have more details.

HerringtonDarkholme commented 9 years ago

May I ask why decorator cannot change the decorated result type from the type being decorated?

For example, MethodDecorator can be alternatively proposed as below:

declare type MethodDecorator = <T, R>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<R> | void;

So decorator can be more flexible to use and works somewhat like a macro. The type of decorated class / field can be updated correspondingly to decorator returning type, so that type safety is still kept.

I know that https://github.com/jonathandturner/brainstorming/blob/master/README.md#c4-defining-a-decorator has stated the returning type should be the same. But type validity isn't a valid argument here, IMHO. First, decorators in JavaScript can alter the class/field type, so there is no compatibility issue between TS and JS. Secondly, decorators are applied statically, TS compiler can reason about the original field type and decorated field type at define site.

mhegazy commented 9 years ago

@HerringtonDarkholme I would say this needs to be tracked in a different suggestion. The main reason is the way the compiler reasons about types is through declarations. once a type is declared it can not change. adding type mutators in the mix means we need to resolve these as we are resolving the declaration which can get hairy.

HerringtonDarkholme commented 9 years ago

@mhegazy so, can I understand this as a compromise for implementation ease, rather than a deliberate design?

According to this proposal, decorators restore the ability to run code at design time, while maintaining a declarative syntax. So the following code shall be valid in future's JavaScript, but not valid in today's TypeScript (however this might be accepted by TS one day, right?).

function SafeCtor(target) {
  var safe = function(...args) {
    var instance = Object.create(target.prototype)
    target.call(instance, ...args)
    return instance
  }
  safe.prototype = target.prototype
  return safe
}

@SafeCtor
class Snake {
  constructor(name) {
    this.name = name
  } 
}

var snake = Snake('python')
alert(snake instanceof Snake)

I know the chances for supporting this feature is small. But I want to confirm this is not a similar story like structural vs nominal typing.

mhegazy commented 9 years ago

The current check is that you have to return something from your decorator that is assignable to the target. the missing part is we are not capturing any additions to the type. I do not think we have an idea on how to do that, but we have not really thought about it either. so i think it is a fair suggestion, but the cost to implement it may be very high.

JsonFreeman commented 9 years ago

Ideally, I think the type returned by the decorator should be identical to the target. There's no telling whether you will use the decorated type in a source or target position in an assignment.

Although I guess really we should merge the decorator return type and the target, kind of like a mixin.

shumy commented 9 years ago

Just some additional functionality idea. Since TypeScript compiler output is JavaScript, there are some similar languages for Java, like Xtend

You can learn about some interesting ideas on this project. Look for Active Annotations "Active annotations allow developers to participate in the translation process of Xtend source code to Java code via library"

Controlling the TypeScript output via decorators will be a really nice feature!

shumy commented 9 years ago

Decorators for interfaces, should be possible.

Gaelan commented 9 years ago

@shumy The problem is that interfaces do not exist at runtime in any way, and decorators run purely at runtime.

sccolbert commented 9 years ago

Is there a reason to treat property decorators differently than method decorators with respect to return value? (Babel doesn't).

What I mean is that, if I want to define functionality for a property via decorator I have to do this:

function decorator(proto, name) {
    Object.defineProperty(proto, name, { value: 42 });
}

class Foo {
    @decorator
    a: number;
}

Whereas Babel requires the descriptor to be returned:

function decorator(proto, name) {
    return { value: 42 };
}

class Foo {
    @decorator
    a;
}

This makes it difficult to write a decorator in TypeScript which is part of a library which will be used from Babel.

I would propose that a property decorator be treated identically to method decorator, and the return value of the decorator should be applied via Object.defineProperty. This would also allow for multiple decorators on a single property, just like a method.

A workaround for now (for Babel compatibility) is to return the descriptor from the decorator in addition to setting the property, but that seems unnecessarily wasteful:

function decorator(proto, name) {
    var d = { value: 42 };
    Object.defineProperty(proto, name, d);
    return d;
}
sccolbert commented 9 years ago

@mhegazy would you happen to have any insight on my comment above?

rbuckton commented 9 years ago

@sccolbert In TypeScript we opted disallow the use of property descriptors for decorators on data properties, as it can lead to issues at runtime due to the fact that any "value" specified by the descriptor will be set on the prototype and not on the instance. While your example above would generally not be an issue, consider the following:

function decorator(proto, name) {
  return { value: new Point(0, 0); }
}

class Foo {
  @decorator
  p: Point;

  setX(x) { this.p.x = 1; }
}

let a = new Foo();
let b = new Foo();
console.log(a.p.x); // 0
b.setX(10);
console.log(a.p.x); // 10 (!)

There is a proposal for a future version of ES to support an "initializer" property on the descriptor, which would be evaluated during the constructor. That would give you the ability to control whether the value is set on the prototype or allocated per-instance.

For now you can work around this limitation by calling Object.defineProperty directly, as per the example in your comment. We will continue to investigate whether to relax this restriction in the future.

sccolbert commented 9 years ago

@rbuckton thanks! Are you guys in talks with Babel to converge on the same semantics? I think that's the most important thing.

JsonFreeman commented 9 years ago

Why would it be useful to use a decorator in order to specify a value for a particular instance? Isn't that what the property initializer is for?

sccolbert commented 9 years ago

My use case is more complicated that the example. I'm actually using the decorator to define a getter which returns an object bound to the this context of the property, so that methods on said object have access to the instance on which it was defined.

You can think of this as emulating the descriptor protocol in Python, where accessing method bar defined on class Foo through instance foo (i.e. foo.bar) invokes the __get__ method of the function which returns a BoundMethod. When that object is called, the underlying function is invoked with self as the first argument, which in this case is foo. Javascript doesn't have this concept, which is why we have to pass around thisArg and call function.bind all over the place.

For my case, I'm not defining methods, but a type-safe signal. Here is the decorator: https://github.com/phosphorjs/phosphor-signaling/blob/1.0.1/src/index.ts#L144

When the property is accessed, it returns an implementation of ISignal which is bound to the this context of the owner. This allows the signal to refer back to the instance on which it was defined: https://github.com/phosphorjs/phosphor-signaling/blob/1.0.1/src/index.ts#L263

So in effect, I'm using the decorator as a shortcut for this verbose equivalent:

class Foo {
  valueChanged: ISignal<number>;
}

defineSignal(Foo.prototype, 'valueChanged');
Koloto commented 9 years ago

@rbuckton

Decorators are not allowed when targeting ES3

Why? I can't see anything that would prevent the use of __decorate in ES3.

cybrown commented 9 years ago

It uses the property descriptor, which is not available in ES3 Le 4 sept. 2015 2:03 PM, "Koloto" notifications@github.com a écrit :

@rbuckton https://github.com/rbuckton

Decorators are not allowed when targeting ES3

Why? I can't see anything that would prevent the use of __decorate in ES3.

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/2249#issuecomment-137717517 .

DavidSouther commented 9 years ago

@sccolbert You might do better for that with ES6 proxies rather than property decorators.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

sccolbert commented 9 years ago

@DavidSouther browsers don't support Proxies yet :(

Koloto commented 9 years ago

@cybrown Yes, it uses the property descriptor for methods and accessors. I tested on plain properties (fields w/o accessors) only. But it seems that decorators can be allowed in ES3 for properties (w/o accessors) and classes. It would be helpful.

Koloto commented 9 years ago

And a fake property descriptor can be used for methods when targeting ES3. Something like { writable: true, enumerable: true, configurable: true }. So I can't see any reason to not support ES3.

JsonFreeman commented 9 years ago

@sccolbert I see. That makes sense. Incidentally, TypeScript is working on improved this typing for functions and methods. I wonder if that would be of any help here. Although I suppose typing is not the issue for you, it's runtime semantics.

sccolbert commented 9 years ago

@JsonFreeman Improved this typing sounds intriguing for some of my other use cases. Do you have any more info on that?

JsonFreeman commented 9 years ago

I think the most developed discussion on this typing is at #3694.

sccolbert commented 9 years ago

Cheers!

TakoLittle commented 9 years ago

error TS1207: Decorators cannot be applied to multiple get/set accessors of the same name.

@get
public get myValue():any{...}

@set
public set myValue(value:any){...}

code above is not allowed even it make more sense compare to

@get
@set
public get myValue():any{...}

public set myValue(value:any){...}
mhegazy commented 9 years ago

Getter and setter are defined in one call to Obect.defineProperty. This rather a js quirk, the declaration of set and get though separate, really are the same property declaration. The error check in the compiler is to alert users when thinking of them separately; the decorators are applied only once to the property descriptor.

TakoLittle commented 9 years ago

just wondering since the compiler can sense the get and set with same name and combine into single Object.defineProperty, why not also combine the decorator? or perhaps an option flag to tell compiler to combine them, and leave a warning message instead of throwing error.

thank you

rbuckton commented 9 years ago

@TakoLittle: The reason we don't do this today partially stems from how decorators are composed. Decorators follow the same principals as Mathematical function composition, where (fg)(x) is composed as f(g(x)). In the same sense, it can be thought that:

@F
@G
class X {}

Is approximately:

F(G(X))

The compositionality of decorators breaks down when you decorate both the getter and the setter:

class C {
  @F
  set X(value) {}

  @G
  get X() {}
}

How do F and G compose here? Is it based purely on document order (i.e. F(G(X)))? Are each set of decorators for the getter and the setter discrete, and then executed in document order (i.e. G(F(X)))? Do get and set imply any specific ordering (i.e. is the get always before the set or vice versa)? Until we're 100% certain the most consistent approach that doesn't surprise users, or have a well documented approach that is part of the decorators proposal with at least stage 2 or better acceptance within ECMA-262, we feel it is best to be more restrictive and error here as it allows us to relax that restriction at a later date without introducing a breaking change that could easily go unnoticed and possibly result in unexpected behaviors at runtime.

TakoLittle commented 9 years ago

@rbuckton thank you so much for detailed explanation TS team great work!! ^^d

omeid commented 8 years ago

Where is the documentation for this? and care to link the implementation commit?

Thanks.

EisenbergEffect commented 8 years ago

@mhegazy What is the status on the implementation of the latest version of the spec. I understand there are some changes there.

mhegazy commented 8 years ago

This issue tracked the original version of the proposal. since this is completed we are closing this issue. for any updates to the spec, we will log new issues and outline all the breaking changes. I do not think the proposal is at a place now to be ready for us to jump on it. We are working closely with @wycats on the new proposal.

mhegazy commented 8 years ago

@omeid, you can find documentation at https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Decorators.md

EisenbergEffect commented 8 years ago

@mhegazy Thank you for the update. I'd love to stay informed. When you create the new issue for the spec update, please link it here so I can be notified and follow. The Aurelia community makes heavy use of decorators and we'll want to synchronize with both TypeScript and Babel on the update. Again, thanks for the great work the TS team is doing!

wclr commented 8 years ago

Function decoration is need of course. Are there also plans for decorating of other objects in the code?