New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Annotations (alignment with Traceur/AtScript) #1557

Closed
JeroMiya opened this Issue Dec 24, 2014 · 31 comments

Comments

Projects
None yet
@JeroMiya

JeroMiya commented Dec 24, 2014

Summary

  • Implement annotations in TypeScript.
  • Align with Traceur and AtScript on syntax and ES5 representation.
  • Implement feature similarly to modules, with the following settings for output representation:
    • -annotations=none (default) - Annotations not supported by default. Produce compiler errors when used.
    • -annotations=atscript - Annotations encoded in the style of traceur/atscript (this proposal)
    • (future) -annotations=ecmascript - Hypothetically, if ES.Vnext were to adopt annotations, this would represent them using the approved ES7 annotation syntax, whatever that would be, as a bridge while TypeScript aligns with the official syntax (similar to how TS modules will transition).
  • In the long term, align with ECMAScript, if an annotation proposal is accepted.

Prior Art/Reference

Motivation

  • This proposal improves alignment between TypeScript and AtScript (and Traceur), improving interoperability between TypeScript code and code using these features of those compilers.
  • Annotations are useful in certain scenarios like dependency injection, domain driven design, decorator pattern, and more.

Differences from AtScript/Traceur

  • Run-time type metadata, runtime type checking, etc... is beyond the scope of this proposal.

Syntax and ES6 Representation

I am proposing the AtScript/Traceur syntax and ES6 representations, necessarily verbatim (minus the type annotations). An annotation can be applied to a function, a class, the constructor of a class, a field of a class, or a function argument:

function:

@MyAnnotation('argument')
function func() {}

// ES6 representation
function func() {}
func.annotate = [ new MyAnnotation('argument')];

A class:

  • Same representation as a function, of course.
@MyAnnotation('argument')
class MyClass {}

// ES6 representation
class MyClass {}
MyClass.annotate = [ new MyAnnotation('argument') ];

A constructor of a class:

  • Note: exactly the same as annotations on the class. Constructors are listed last in the list in the ES6 representation, after class annotations.
@MyClassAnnotation('argument')
class MyClass {
   @MyConstructorAnnotation('argument')
   constructor() {}
}

// ES6 representation
class MyClass {
  constructor() {}
}
MyClass.annotate = [ new MyClassAnnotation('argument'), new MyConstructorAnnotation('argument') ];

A field of a class:

  • Note: in AtScript/Traceur, the objects to the right of the field name also have an 'is' field at the same level as the 'annotate' field. I have omitted the 'is' fields below (RTTI is out-of-scope for this proposal).
class MyClass<T> {
  @MyFieldAnnotation('argument')
  field: T = null;
}

// ES6 representation:
class MyClass {
  field = null;
}
MyClass.properties = {
  'field': { annotate: [ new MyFieldAnnotation('argument') ] }
};

Arguments to a function:

  • Including arguments to constructors.
  • Note: In Traceur/AtScript, objects in the parameters array also have an 'is' field which is a reference to the type of the argument. That is omitted from the following examples (RTTI is out-of-scope for this proposal).
function func<T>(@MyFuncArgumentAnnotation('argument') arg: T): void {}

class MyClass<T> {
  constructor(@MyConstructorArgumentAnnotation('argument') arg: T) {}
}

// ES6 representation
function func(arg) {}
func.parameters = [ { annotate: [ new MyFuncArgumentAnnotation('argument') ] }];

class MyClass {
  constructor(arg) {}
}
MyClass.parameters = [ { annotate: [ new MyConstructorArgumentAnnotation('argument') ] }];

Extensions to lib.d.ts to support typing of annotations

  • e.g. adding 'properties', 'annotate', and 'parameters' field typings to Function interface.
  • Can be done by a third party/DT/etc... without changes to the compiler.
@spion

This comment has been minimized.

Show comment
Hide comment
@spion

spion Dec 25, 2014

My 2c: as they're defined in AtScript now, annotations have a couple of nasty problems

  1. They don't compose. If you need to add 10 annotations to 5 fields, and they only differ sligtly by one parameter, you will need to copy-paste all 10 fields, 5 times. You can't abstract them behind a single annotation.
  2. They use new. A factory function is more appropriate - that way they wont have to be defined as classes.
  3. They're passive and therefore limited in power. The standard practice in JS is to wrap functions/objects/properties using other functions, e.g.
Item.prototype.method = wrapper(function() { 
    //... 
});

Angular, Ember and other frameworks already do this kind of wrapping (e.g. see angularModule.factory or ember computed properties, etc). If annotations were added to the language, I'd think they should reflect this. Its quite easy to implement passive annotations on top of it.

One of the TC39 proposals mentioned in the document above works like this. Its similar to Python's and I think thats what AtScript, TypeScript and ES7 should aim for.

spion commented Dec 25, 2014

My 2c: as they're defined in AtScript now, annotations have a couple of nasty problems

  1. They don't compose. If you need to add 10 annotations to 5 fields, and they only differ sligtly by one parameter, you will need to copy-paste all 10 fields, 5 times. You can't abstract them behind a single annotation.
  2. They use new. A factory function is more appropriate - that way they wont have to be defined as classes.
  3. They're passive and therefore limited in power. The standard practice in JS is to wrap functions/objects/properties using other functions, e.g.
Item.prototype.method = wrapper(function() { 
    //... 
});

Angular, Ember and other frameworks already do this kind of wrapping (e.g. see angularModule.factory or ember computed properties, etc). If annotations were added to the language, I'd think they should reflect this. Its quite easy to implement passive annotations on top of it.

One of the TC39 proposals mentioned in the document above works like this. Its similar to Python's and I think thats what AtScript, TypeScript and ES7 should aim for.

@JeroMiya

This comment has been minimized.

Show comment
Hide comment
@JeroMiya

JeroMiya Dec 25, 2014

As an aside, using new with a factory function will work as expected. The return value of the factory function will be used. However, I concede that it isn't idiomatic javascript, and causes an unnecessary object creation.

That being said, I think I can address 1 and 2 in one or two ways (one would require the ES6 splat operator, the other wouldn't):

With just the proposed syntax above, note that you can still set annotations directly, giving you control over composition and factory vs new:

function getCommonAnnotations() {
  return [new A1(), new A2(), a3FactoryFunc()];
}
function annotationFactoryFunc() { etc... }
class MyClass {
    // I'm using the spread operator here, but I could have just created a wrapper func
    static annotations = [...getCommonAnnotations(), annotationFactoryFunc()];
}

Or, if you really want composition in the syntax, then I would propose the following alternate array syntax, which would require the ES6 spread operator. The following would be equivalent to the above sample:

@[...getCommonAnnotations(), annotationFactoryFunc()]
class MyClass {}

I think either of these is sufficient to overcome concerns 1 and 2. With respect to your third comment, I specifically avoided "decorators" in this proposal. First of all, you don't really need new syntax for decorators - as you said, function wrappers give you everything you need, and the syntax is already about as concise as it could possibly be.

Secondly, mutating decorators have implications for typing. If a decorator modifies the prototype of the class it is applied to, say adding a set of functions, how does TypeScript know? You could annotate the class with an interface, but you'd have to stub everything that the decorator adds to your class first, because the TypeScript compiler can't tell that a decorator added implementation for the interface. And even if you stubbed the fields out, the interface check is effectively turned off, because you can't enforce that the decorator is adding fields/methods that match the interface anyway. In those kinds of cases, I think using the type system for that kind of type augmentation is a superior option, especially if we support mixins.

Finally, I think there really should be a separation between decorators and annotations. Decorator functions/wrappers are good candidates for being consumers of the metadata. Making annotations inert also improves their reliability - It would cause a lot of confusion to have to track down issues related to mutating annotations. There's no way to document to the type system (and thus the programmer), what it is that the mutating annotation is doing to your type, field, function argument, or method. Thus, I propose a more conservative passive approach to annotations.

JeroMiya commented Dec 25, 2014

As an aside, using new with a factory function will work as expected. The return value of the factory function will be used. However, I concede that it isn't idiomatic javascript, and causes an unnecessary object creation.

That being said, I think I can address 1 and 2 in one or two ways (one would require the ES6 splat operator, the other wouldn't):

With just the proposed syntax above, note that you can still set annotations directly, giving you control over composition and factory vs new:

function getCommonAnnotations() {
  return [new A1(), new A2(), a3FactoryFunc()];
}
function annotationFactoryFunc() { etc... }
class MyClass {
    // I'm using the spread operator here, but I could have just created a wrapper func
    static annotations = [...getCommonAnnotations(), annotationFactoryFunc()];
}

Or, if you really want composition in the syntax, then I would propose the following alternate array syntax, which would require the ES6 spread operator. The following would be equivalent to the above sample:

@[...getCommonAnnotations(), annotationFactoryFunc()]
class MyClass {}

I think either of these is sufficient to overcome concerns 1 and 2. With respect to your third comment, I specifically avoided "decorators" in this proposal. First of all, you don't really need new syntax for decorators - as you said, function wrappers give you everything you need, and the syntax is already about as concise as it could possibly be.

Secondly, mutating decorators have implications for typing. If a decorator modifies the prototype of the class it is applied to, say adding a set of functions, how does TypeScript know? You could annotate the class with an interface, but you'd have to stub everything that the decorator adds to your class first, because the TypeScript compiler can't tell that a decorator added implementation for the interface. And even if you stubbed the fields out, the interface check is effectively turned off, because you can't enforce that the decorator is adding fields/methods that match the interface anyway. In those kinds of cases, I think using the type system for that kind of type augmentation is a superior option, especially if we support mixins.

Finally, I think there really should be a separation between decorators and annotations. Decorator functions/wrappers are good candidates for being consumers of the metadata. Making annotations inert also improves their reliability - It would cause a lot of confusion to have to track down issues related to mutating annotations. There's no way to document to the type system (and thus the programmer), what it is that the mutating annotation is doing to your type, field, function argument, or method. Thus, I propose a more conservative passive approach to annotations.

@spion

This comment has been minimized.

Show comment
Hide comment
@spion

spion Dec 25, 2014

Yes, its important that all language features compose easily so that we don't end up with something like this. The spread operator solution would be okay but it kinda breaks the abstraction barrier. The developer should not have to know whether the annotation they're using is composite or not.

First of all, you don't really need new syntax for decorators - as you said, function wrappers give you everything you need, and the syntax is already about as concise as it could possibly be.

New syntax is necessary mostly because they are not allowed inside class definitions in ES6. This is also why Yehuda proposed them when they switched Ember to ES6 (iirc - i can't find the right esdiscuss thread)

Secondly, mutating decorators have implications for typing. If a decorator modifies the prototype of the class it is applied to, say adding a set of functions, how does TypeScript know? You could annotate the class with an interface, but you'd have to stub everything that the decorator adds to your class first, because the TypeScript compiler can't tell that a decorator added implementation for the interface. And even if you stubbed the fields out, the interface check is effectively turned off, because you can't enforce that the decorator is adding fields/methods that match the interface anyway. In those kinds of cases, I think using the type system for that kind of type augmentation is a superior option, especially if we support mixins.

You're right that there would be type problems with decorators. Mutation would be tricky to model. Maybe decorators could always return new values instead mutating them, and intersection types (similar to union but using &) could be used to model augmentation. Or maybe the mixin mechanism will be able to help there...

I agree with most of the benefits of inert/passive annotations that you mentioned.

Overall, I feel it may be too early to add annotations to TS. The proposal is (barely?) in the strawman stage for ES7+. Angular/AtScript are just now starting to experiment with what annotations would mean - it may still turn out that their design is flawed...

spion commented Dec 25, 2014

Yes, its important that all language features compose easily so that we don't end up with something like this. The spread operator solution would be okay but it kinda breaks the abstraction barrier. The developer should not have to know whether the annotation they're using is composite or not.

First of all, you don't really need new syntax for decorators - as you said, function wrappers give you everything you need, and the syntax is already about as concise as it could possibly be.

New syntax is necessary mostly because they are not allowed inside class definitions in ES6. This is also why Yehuda proposed them when they switched Ember to ES6 (iirc - i can't find the right esdiscuss thread)

Secondly, mutating decorators have implications for typing. If a decorator modifies the prototype of the class it is applied to, say adding a set of functions, how does TypeScript know? You could annotate the class with an interface, but you'd have to stub everything that the decorator adds to your class first, because the TypeScript compiler can't tell that a decorator added implementation for the interface. And even if you stubbed the fields out, the interface check is effectively turned off, because you can't enforce that the decorator is adding fields/methods that match the interface anyway. In those kinds of cases, I think using the type system for that kind of type augmentation is a superior option, especially if we support mixins.

You're right that there would be type problems with decorators. Mutation would be tricky to model. Maybe decorators could always return new values instead mutating them, and intersection types (similar to union but using &) could be used to model augmentation. Or maybe the mixin mechanism will be able to help there...

I agree with most of the benefits of inert/passive annotations that you mentioned.

Overall, I feel it may be too early to add annotations to TS. The proposal is (barely?) in the strawman stage for ES7+. Angular/AtScript are just now starting to experiment with what annotations would mean - it may still turn out that their design is flawed...

@JeroMiya

This comment has been minimized.

Show comment
Hide comment
@JeroMiya

JeroMiya Dec 25, 2014

We can't make too many assumptions about how application code will consume annotations, but, assuming it checks for matching constructors recursively up the prototype chain - couldn't we just use the type system to compose annotations to avoid the situation you linked? It would of course require mixins (specifically, mixins which rewrite the prototype chain in a linear fashion, using a linearization algorithm as is done in scala), but you could just create sub-type annotations that mix in the aggregation of common annotations. You wouldn't then need the spread operator, explicit, or alternate array syntax to make it work.

class A {}
class B {}
class C {
  constructor(private value: string) {}
}

// compose!
// note: hypothetical syntax for mixins
class CompositeAnnotation with A, B, C {
  constructor() {
    // note: hypothetical mixin base class initialization syntax
    super<B>('specialized value');
  }
}

@CompositeAnnotation
class MyClass {}

Also, third party libraries could provide their own composition functionality for attributes, say with an aggregate annotation that takes an array of annotations.

JeroMiya commented Dec 25, 2014

We can't make too many assumptions about how application code will consume annotations, but, assuming it checks for matching constructors recursively up the prototype chain - couldn't we just use the type system to compose annotations to avoid the situation you linked? It would of course require mixins (specifically, mixins which rewrite the prototype chain in a linear fashion, using a linearization algorithm as is done in scala), but you could just create sub-type annotations that mix in the aggregation of common annotations. You wouldn't then need the spread operator, explicit, or alternate array syntax to make it work.

class A {}
class B {}
class C {
  constructor(private value: string) {}
}

// compose!
// note: hypothetical syntax for mixins
class CompositeAnnotation with A, B, C {
  constructor() {
    // note: hypothetical mixin base class initialization syntax
    super<B>('specialized value');
  }
}

@CompositeAnnotation
class MyClass {}

Also, third party libraries could provide their own composition functionality for attributes, say with an aggregate annotation that takes an array of annotations.

@jonathandturner

This comment has been minimized.

Show comment
Hide comment
@jonathandturner

jonathandturner Dec 29, 2014

Contributor

Good stuff! Annotations are definitely something we're interested in for TypeScript. I'm going to ping @mhegazy, who has been working on an annotations proposal, too. This would be right up his alley.

Contributor

jonathandturner commented Dec 29, 2014

Good stuff! Annotations are definitely something we're interested in for TypeScript. I'm going to ping @mhegazy, who has been working on an annotations proposal, too. This would be right up his alley.

@EisenbergEffect

This comment has been minimized.

Show comment
Hide comment
@EisenbergEffect

EisenbergEffect Jan 16, 2015

It should be noted that there is broad experience with annotations. This feature is based on Java annotations and C# attributes and the use cases are similar. Additionally, the Angular and Aurelia teams have been using them successfully for at least a year now and the usage patterns are pretty well known. Aurelia has a metadata abstraction library that handles location of annotations and manipulation including inheritance chains.

I don't see composition as being essential. It's not something people did on other platforms and the Angular and Aurelia teams haven't needed that. Adding and reusing massive amounts of annotations doesn't seem like something common. I think the spread operator can handle this nicely in the very few number of cases where it comes up.

I see decorators as a fundamentally different concept. Annotations are about having a standard way to associate metadata with functions and class members, that's pretty much it.

Note: I'm not sure if AtScript/Tracuer is up to date with the current spec they have published. If you try it out on the online REPL it generates the older syntax. Probably should check with them to see what the status of that is and why it doesn't match their proposal.

EisenbergEffect commented Jan 16, 2015

It should be noted that there is broad experience with annotations. This feature is based on Java annotations and C# attributes and the use cases are similar. Additionally, the Angular and Aurelia teams have been using them successfully for at least a year now and the usage patterns are pretty well known. Aurelia has a metadata abstraction library that handles location of annotations and manipulation including inheritance chains.

I don't see composition as being essential. It's not something people did on other platforms and the Angular and Aurelia teams haven't needed that. Adding and reusing massive amounts of annotations doesn't seem like something common. I think the spread operator can handle this nicely in the very few number of cases where it comes up.

I see decorators as a fundamentally different concept. Annotations are about having a standard way to associate metadata with functions and class members, that's pretty much it.

Note: I'm not sure if AtScript/Tracuer is up to date with the current spec they have published. If you try it out on the online REPL it generates the older syntax. Probably should check with them to see what the status of that is and why it doesn't match their proposal.

@spion

This comment has been minimized.

Show comment
Hide comment
@spion

spion Jan 17, 2015

@EisenbergEffect As long as a single library is the consumer (e.g. the framework), I agree. However when there are multiple consumers, things might quickly get out of control.

FWIW the example I linked contains the following code:

@XmlElementWrapper(name="orders")
@XmlJavaTypeAdapter(OrderJaxbAdapter.class)
@XmlElements({
   @XmlElement(name="order_2",type=Order2.class),
   @XmlElement(name="old_order",type=OldOrder.class)
})
@JsonIgnore
@JsonProperty
@NotNull
@ManyToMany
@Fetch(FetchMode.SUBSELECT)
@JoinTable(
    name = "customer_order",
    joinColumns = {
        @JoinColumn(name = "customer_id", referencedColumnName = "id")
    },
    inverseJoinColumns = {
        @JoinColumn(name = "order_id", referencedColumnName = "id")
    }
)
private List orders;

and composition is important because it would be great if it was possible to reduce it to something at the same level of abstraction e.g.

@SerializableTo(name="orders");
@BelongsTo('customer')
private List orders;

spion commented Jan 17, 2015

@EisenbergEffect As long as a single library is the consumer (e.g. the framework), I agree. However when there are multiple consumers, things might quickly get out of control.

FWIW the example I linked contains the following code:

@XmlElementWrapper(name="orders")
@XmlJavaTypeAdapter(OrderJaxbAdapter.class)
@XmlElements({
   @XmlElement(name="order_2",type=Order2.class),
   @XmlElement(name="old_order",type=OldOrder.class)
})
@JsonIgnore
@JsonProperty
@NotNull
@ManyToMany
@Fetch(FetchMode.SUBSELECT)
@JoinTable(
    name = "customer_order",
    joinColumns = {
        @JoinColumn(name = "customer_id", referencedColumnName = "id")
    },
    inverseJoinColumns = {
        @JoinColumn(name = "order_id", referencedColumnName = "id")
    }
)
private List orders;

and composition is important because it would be great if it was possible to reduce it to something at the same level of abstraction e.g.

@SerializableTo(name="orders");
@BelongsTo('customer')
private List orders;
@EisenbergEffect

This comment has been minimized.

Show comment
Hide comment
@EisenbergEffect

EisenbergEffect Jan 17, 2015

@spion I agree that we don't want to see that. Personally, I would choose to solve this issue at the framework level by making something like @SerializableTo internally aggregate the other annotations. Something like @JeroMiya shows would also be interesting, especially since Mixins would be a nice feature in their own right.

That said, I would be willing to concede to the decorator approach, especially if that looked like the direction that TC39 was going to move in. Since I'm still interested in metadata mostly, it would be pretty easy for me to author a set of decorators that simply put data in a location that my framework knows to look. I could publish that library as a simple ES6 module and then developers working with languages that support it could simply import the library and use them. The more I think about it....I'm liking this decorator approach. It would provide a lot more flexibility in the end.

EisenbergEffect commented Jan 17, 2015

@spion I agree that we don't want to see that. Personally, I would choose to solve this issue at the framework level by making something like @SerializableTo internally aggregate the other annotations. Something like @JeroMiya shows would also be interesting, especially since Mixins would be a nice feature in their own right.

That said, I would be willing to concede to the decorator approach, especially if that looked like the direction that TC39 was going to move in. Since I'm still interested in metadata mostly, it would be pretty easy for me to author a set of decorators that simply put data in a location that my framework knows to look. I could publish that library as a simple ES6 module and then developers working with languages that support it could simply import the library and use them. The more I think about it....I'm liking this decorator approach. It would provide a lot more flexibility in the end.

@EisenbergEffect

This comment has been minimized.

Show comment
Hide comment
@EisenbergEffect

EisenbergEffect Jan 17, 2015

@spion How would the decorator proposal handle constructor parameters and class fields? It seems that it breaks down in those scenarios... (I don't personally have an interest in that...mostly methods and accessors, but just curious.)

EisenbergEffect commented Jan 17, 2015

@spion How would the decorator proposal handle constructor parameters and class fields? It seems that it breaks down in those scenarios... (I don't personally have an interest in that...mostly methods and accessors, but just curious.)

@domenic

This comment has been minimized.

Show comment
Hide comment
@domenic

domenic Jan 17, 2015

It's important to note that annotations have never been proposed to TC39 and should not be considered in any way standards-track. In contrast, decorators have been, and are accepted as a proposal that is now dependent on their champion to move forward.

domenic commented Jan 17, 2015

It's important to note that annotations have never been proposed to TC39 and should not be considered in any way standards-track. In contrast, decorators have been, and are accepted as a proposal that is now dependent on their champion to move forward.

@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Jan 17, 2015

That said, I would be willing to concede to the decorator approach, especially if that looked like the direction that TC39 was going to move in. Since I'm still interested in metadata mostly, it would be pretty easy for me to author a set of decorators that simply put data in a location that my framework knows to look.

Yes, exactly. Decorators provide a strict superset of the capabilities of metadata annotations.

wycats commented Jan 17, 2015

That said, I would be willing to concede to the decorator approach, especially if that looked like the direction that TC39 was going to move in. Since I'm still interested in metadata mostly, it would be pretty easy for me to author a set of decorators that simply put data in a location that my framework knows to look.

Yes, exactly. Decorators provide a strict superset of the capabilities of metadata annotations.

@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Jan 17, 2015

How would the decorator proposal handle constructor parameters and class fields? It seems that it breaks down in those scenarios...

Can you say more about what specific use-case you have in mind? It sounds like class-level annotations would do the trick?

Can you show me some ES6 boilerplate you would want to sugar up using decorators?

wycats commented Jan 17, 2015

How would the decorator proposal handle constructor parameters and class fields? It seems that it breaks down in those scenarios...

Can you say more about what specific use-case you have in mind? It sounds like class-level annotations would do the trick?

Can you show me some ES6 boilerplate you would want to sugar up using decorators?

@Arnavion

This comment has been minimized.

Show comment
Hide comment
@Arnavion

Arnavion Jan 18, 2015

Contributor
  1. Should annotations be allowed on interfaces?
  2. If (1) is yes, are they applied to a class that implements the interface? Are annotations on a member of the interface applied to the corresponding member of the class that implements the interface?
  3. If (2) is yes, What happens if there are two interfaces with different annotations, or two interfaces with the same member but different annotations on that member? Specifically, which order are the annotations applied in? (Order of discovery / undefined)
  4. If (2) is yes, a class instance is assignable to an interface even if it doesn't explicitly implement it, because of structural typing. However the actual class emit won't have this annotation. An object literal with the same members is also assignable for the same reason. Will this be a problem? One should not expect to find interface annotations on any object that satisfies the interface, but the answer depends on the expectations of people who annotated the interface.
Contributor

Arnavion commented Jan 18, 2015

  1. Should annotations be allowed on interfaces?
  2. If (1) is yes, are they applied to a class that implements the interface? Are annotations on a member of the interface applied to the corresponding member of the class that implements the interface?
  3. If (2) is yes, What happens if there are two interfaces with different annotations, or two interfaces with the same member but different annotations on that member? Specifically, which order are the annotations applied in? (Order of discovery / undefined)
  4. If (2) is yes, a class instance is assignable to an interface even if it doesn't explicitly implement it, because of structural typing. However the actual class emit won't have this annotation. An object literal with the same members is also assignable for the same reason. Will this be a problem? One should not expect to find interface annotations on any object that satisfies the interface, but the answer depends on the expectations of people who annotated the interface.
@EisenbergEffect

This comment has been minimized.

Show comment
Hide comment
@EisenbergEffect

EisenbergEffect Jan 18, 2015

@wycats Actually, I think you are correct. Class-level decorators could easily add constructor parameter metadata and property metadata to a known location like anything else.

Are you championing this feature for ES7? What is the status of that work? Do you need/want any help with that?

EisenbergEffect commented Jan 18, 2015

@wycats Actually, I think you are correct. Class-level decorators could easily add constructor parameter metadata and property metadata to a known location like anything else.

Are you championing this feature for ES7? What is the status of that work? Do you need/want any help with that?

@JeroMiya

This comment has been minimized.

Show comment
Hide comment
@JeroMiya

JeroMiya Jan 18, 2015

It was not my intent to allow annotations on interfaces. For this use case, I envisioned inheritance and perhaps mixins would provide the necessary attribute inheritance. Attributes would be aggregated starting with the base class and traveling down the inheritance chain. Since there isn't yet a formal mixin proposal, I'd have to guess that they'd roughly follow Scala traits in the algorithm for 'stacking' the mixins linearly. In that case, the final order of the aggregated attributes would be determined from the stack order (highest base to lowest).

Also, attributes would not modify the structural type of the class or function. The 'properties', 'annotate', and 'parameters' fields would be added to the base Function interface as optional fields. Annotations are applied to Functions and modify those specific fields. They don't have any effect on instances of the class or the return value of the function, though you can read the attributes through the instance object's constructor field.

Finally, just wanted to point out that it might be somewhat awkward to replicate attribute inheritance using decorators, given that the best place to do that would be within __extends. Decorator functions would need access to _super at the very least. Though, perhaps a single decorator, say 'Annotate' could replicate annotations:

// using decorator syntax from TC-39 discussion
class A {
 + annotate([new AnnotationA(), new AnnotationB()])
}

class B extends A {
 + annotate([new AnnotationC(), new AnnotationD()]) // how does annotate know about A?
}

It might work, I suppose, but it seems a little awkward for an important use case. Is there any reason why we couldn't adopt both annotations and decorators (using for example @ for annotations and - or + for decorators)? Annotations could be type-aware syntax sugar for an 'annotate' decorator.

JeroMiya commented Jan 18, 2015

It was not my intent to allow annotations on interfaces. For this use case, I envisioned inheritance and perhaps mixins would provide the necessary attribute inheritance. Attributes would be aggregated starting with the base class and traveling down the inheritance chain. Since there isn't yet a formal mixin proposal, I'd have to guess that they'd roughly follow Scala traits in the algorithm for 'stacking' the mixins linearly. In that case, the final order of the aggregated attributes would be determined from the stack order (highest base to lowest).

Also, attributes would not modify the structural type of the class or function. The 'properties', 'annotate', and 'parameters' fields would be added to the base Function interface as optional fields. Annotations are applied to Functions and modify those specific fields. They don't have any effect on instances of the class or the return value of the function, though you can read the attributes through the instance object's constructor field.

Finally, just wanted to point out that it might be somewhat awkward to replicate attribute inheritance using decorators, given that the best place to do that would be within __extends. Decorator functions would need access to _super at the very least. Though, perhaps a single decorator, say 'Annotate' could replicate annotations:

// using decorator syntax from TC-39 discussion
class A {
 + annotate([new AnnotationA(), new AnnotationB()])
}

class B extends A {
 + annotate([new AnnotationC(), new AnnotationD()]) // how does annotate know about A?
}

It might work, I suppose, but it seems a little awkward for an important use case. Is there any reason why we couldn't adopt both annotations and decorators (using for example @ for annotations and - or + for decorators)? Annotations could be type-aware syntax sugar for an 'annotate' decorator.

@EisenbergEffect

This comment has been minimized.

Show comment
Hide comment
@EisenbergEffect

EisenbergEffect Jan 19, 2015

Why does inheritance of annotations need to be "implemented"? This seems like a library concern. For example, it's common that the consumer of the metadata will want to decide whether or not to search the base class or not. Sometimes you do, sometimes you don't.

EisenbergEffect commented Jan 19, 2015

Why does inheritance of annotations need to be "implemented"? This seems like a library concern. For example, it's common that the consumer of the metadata will want to decide whether or not to search the base class or not. Sometimes you do, sometimes you don't.

@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Jan 19, 2015

@EisenbergEffect Yes I am championing this feature for ES7 and would love help. Email me? (wycats@gmail.com)

wycats commented Jan 19, 2015

@EisenbergEffect Yes I am championing this feature for ES7 and would love help. Email me? (wycats@gmail.com)

@JeroMiya

This comment has been minimized.

Show comment
Hide comment
@JeroMiya

JeroMiya Jan 19, 2015

@EisenbergEffect Good point. If we're talking about annotations and not decorators, I would think it's the class itself that should decide how it inherits annotations, with it inheriting by default (it seems to me the most sensible default, or at least the least likely to surprise). In C#, the inheritance is specified at the annotation level, as I think you are proposing, but in this proposal there are no restrictions or specifications on what an annotation is - any constructor function works, so there's no metadata on the annotation for specifying whether it should be inherited or not. Alternatively the framework could just walk up the prototype chain.

If we're talking about decorators, then of course these are only 'executed' on the base class, and any modifications are inherited by sub-classes in the normal way, though as I said earlier it would be difficult to impossible to represent decorator type modifications in the type system automatically.

Maybe we can re-use the 'declare' keyword in the context of member variables (in classes and interfaces) to declare that some field exists without actually implementing it. Then we could add type information for modifications made by the decorator without using placeholders that presumably would be overwritten by the decorator:

interface IDeclaredA {
  // currently a syntax error, unexpected token
  declare var a: string; 
}
class A implements IDeclaredA {
  // declared members are not enforced in the implementation and no
  // code is generated for them
  + addAFieldDecorator('a', 'string', 'initialValue') // this decorator adds a field to the prototype
}
var x = new A();
alert(x.a);

JeroMiya commented Jan 19, 2015

@EisenbergEffect Good point. If we're talking about annotations and not decorators, I would think it's the class itself that should decide how it inherits annotations, with it inheriting by default (it seems to me the most sensible default, or at least the least likely to surprise). In C#, the inheritance is specified at the annotation level, as I think you are proposing, but in this proposal there are no restrictions or specifications on what an annotation is - any constructor function works, so there's no metadata on the annotation for specifying whether it should be inherited or not. Alternatively the framework could just walk up the prototype chain.

If we're talking about decorators, then of course these are only 'executed' on the base class, and any modifications are inherited by sub-classes in the normal way, though as I said earlier it would be difficult to impossible to represent decorator type modifications in the type system automatically.

Maybe we can re-use the 'declare' keyword in the context of member variables (in classes and interfaces) to declare that some field exists without actually implementing it. Then we could add type information for modifications made by the decorator without using placeholders that presumably would be overwritten by the decorator:

interface IDeclaredA {
  // currently a syntax error, unexpected token
  declare var a: string; 
}
class A implements IDeclaredA {
  // declared members are not enforced in the implementation and no
  // code is generated for them
  + addAFieldDecorator('a', 'string', 'initialValue') // this decorator adds a field to the prototype
}
var x = new A();
alert(x.a);
@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Jan 19, 2015

If we're talking about decorators, then of course these are only 'executed' on the base class, and any modifications are inherited by sub-classes in the normal way, though as I said earlier it would be difficult to impossible to represent decorator type modifications in the type system automatically

As I said to the TypeScript team, I think the exact opposite of this is true.

  1. In practice, some people will use either kind of annotation ("data-only" or "decorators") to mutate the class and its properties. With data-only decorators, a dependency injection system or simple functions can be used to do the modifications.
  2. When using data-only annotations, the nature of the modification is completely opaque to the type system. From the perspective of the type system, no changes have been made (only data has been added) so any mutation would be invisible.
  3. When using decorator annotations (which take in a property descriptor and return a new property descriptor), it is at least POSSIBLE to describe the transformation in terms of a type transformation.
  4. In short, because data-only annotations lie about their intentions ("don't worry... I'm just adding data ;) ;)"), they at first appear to be nicer for type systems, but actually hide mutations in JavaScript code that would be incredibly difficult to type.

For example, consider a hypothetical readonly annotation:

function nonconfigurable(prototype, name, descriptor) {
  descriptor.configurable = false;
  return descriptor;
}

And this usage:

class Article {
  @nonconfigurable
  length() { /* implementation */ }
}

In this case, the @nonconfigurable annotation is indeed changing the nature of the length method, but it's doing so in a very specific, very structured way.

You could imagine TypeScript providing a way for the nonconfigurable annotation to express this change (and the type system would propagate those changes through a stack of annotations). It's much harder to imagine TypeScript being able to handle "data-only" annotations that were actually being used by an external system to execute those same changes.

And just to be clear, whichever kind of annotation ends up landing, Ember intends to use the mechanism to do precisely these kinds of mutations. The only question is whether a library that does so has a prayer of eventually being compatible with TypeScript or not. I want the answer to be yes.

wycats commented Jan 19, 2015

If we're talking about decorators, then of course these are only 'executed' on the base class, and any modifications are inherited by sub-classes in the normal way, though as I said earlier it would be difficult to impossible to represent decorator type modifications in the type system automatically

As I said to the TypeScript team, I think the exact opposite of this is true.

  1. In practice, some people will use either kind of annotation ("data-only" or "decorators") to mutate the class and its properties. With data-only decorators, a dependency injection system or simple functions can be used to do the modifications.
  2. When using data-only annotations, the nature of the modification is completely opaque to the type system. From the perspective of the type system, no changes have been made (only data has been added) so any mutation would be invisible.
  3. When using decorator annotations (which take in a property descriptor and return a new property descriptor), it is at least POSSIBLE to describe the transformation in terms of a type transformation.
  4. In short, because data-only annotations lie about their intentions ("don't worry... I'm just adding data ;) ;)"), they at first appear to be nicer for type systems, but actually hide mutations in JavaScript code that would be incredibly difficult to type.

For example, consider a hypothetical readonly annotation:

function nonconfigurable(prototype, name, descriptor) {
  descriptor.configurable = false;
  return descriptor;
}

And this usage:

class Article {
  @nonconfigurable
  length() { /* implementation */ }
}

In this case, the @nonconfigurable annotation is indeed changing the nature of the length method, but it's doing so in a very specific, very structured way.

You could imagine TypeScript providing a way for the nonconfigurable annotation to express this change (and the type system would propagate those changes through a stack of annotations). It's much harder to imagine TypeScript being able to handle "data-only" annotations that were actually being used by an external system to execute those same changes.

And just to be clear, whichever kind of annotation ends up landing, Ember intends to use the mechanism to do precisely these kinds of mutations. The only question is whether a library that does so has a prayer of eventually being compatible with TypeScript or not. I want the answer to be yes.

@EisenbergEffect

This comment has been minimized.

Show comment
Hide comment
@EisenbergEffect

EisenbergEffect Jan 19, 2015

I tend to agree with @wycats on this. One of the big uses for annotations is to provide metadata to a runtime system that ends up doing some sort of metaprogramming based on that data. But the alterations being made to the annotated objects are in no way discoverable by the TypeScript compiler. However, by using decorators, the metaprogramming can be moved out of the framework proper and into the decorator itself...and defined in such a way that TypeScript can understand the transformation. For non-metaprogramming use cases, the decorator can still add meta-data to a known location on the object. This could also be declared as part of the TS decorator definition also.

EisenbergEffect commented Jan 19, 2015

I tend to agree with @wycats on this. One of the big uses for annotations is to provide metadata to a runtime system that ends up doing some sort of metaprogramming based on that data. But the alterations being made to the annotated objects are in no way discoverable by the TypeScript compiler. However, by using decorators, the metaprogramming can be moved out of the framework proper and into the decorator itself...and defined in such a way that TypeScript can understand the transformation. For non-metaprogramming use cases, the decorator can still add meta-data to a known location on the object. This could also be declared as part of the TS decorator definition also.

@JeroMiya

This comment has been minimized.

Show comment
Hide comment
@JeroMiya

JeroMiya Jan 19, 2015

We are assuming that there is a reliable way to model type transformations done by arbitrary decorators in the first place. Consider that some decorators may be implemented in a stateful way, or take some framework-specific config object and make dynamic modifications based on that object. I don't have high confidence that any but the simplest transformations could be modeled.

Even assuming it were possible to model complex transformations based on dynamic and possibly stateful data - why wouldn't we also be able to model type transformations from arbitrary framework functions acting on annotated types? If you can handle type transformations based on a config object, for example, you can handle type transformations based off of annotations.

Finally, many (I might hazard a guess and say "most") use cases for annotations do not make any type transformations at all. For example, in the dependency injection use case, the annotations are used to inject values into constructor arguments or to set field values after object construction, leaving the annotated type unchanged. For the serialization or database mapping use case, they affect how instances of the type are serialized, or how fields from the type are mapped to a database, etc... In an MVC framework they could be used for specifying routing, or how to map HTTP headers or URI parameters to action methods in a controller. If you take a look at annotations in other languages like C# or Java, it is very rare that these annotations actually cause a change in the types they are applied to. Even when some IL rewriting happens as a post build step, the type is usually left unchanged - e.g. injecting some code into a method or property but leaving its signature alone.

JeroMiya commented Jan 19, 2015

We are assuming that there is a reliable way to model type transformations done by arbitrary decorators in the first place. Consider that some decorators may be implemented in a stateful way, or take some framework-specific config object and make dynamic modifications based on that object. I don't have high confidence that any but the simplest transformations could be modeled.

Even assuming it were possible to model complex transformations based on dynamic and possibly stateful data - why wouldn't we also be able to model type transformations from arbitrary framework functions acting on annotated types? If you can handle type transformations based on a config object, for example, you can handle type transformations based off of annotations.

Finally, many (I might hazard a guess and say "most") use cases for annotations do not make any type transformations at all. For example, in the dependency injection use case, the annotations are used to inject values into constructor arguments or to set field values after object construction, leaving the annotated type unchanged. For the serialization or database mapping use case, they affect how instances of the type are serialized, or how fields from the type are mapped to a database, etc... In an MVC framework they could be used for specifying routing, or how to map HTTP headers or URI parameters to action methods in a controller. If you take a look at annotations in other languages like C# or Java, it is very rare that these annotations actually cause a change in the types they are applied to. Even when some IL rewriting happens as a post build step, the type is usually left unchanged - e.g. injecting some code into a method or property but leaving its signature alone.

@EisenbergEffect

This comment has been minimized.

Show comment
Hide comment
@EisenbergEffect

EisenbergEffect Jan 19, 2015

@JeroMiya You are looking at this from the perspective of C#. Those are valid use cases, but you need to consider how similar techniques were used elsewhere, like in Ruby for example. In this case similar ideas are being used to do metaprogramming which absolutely changed the definition of the object and I would argue is a key aspect of the success of the language and of frameworks like RoR.

I see a couple of important points that need to be considered for the language design:

  1. Decorators can accomplish everything that annotations can, but annotations cannot accomplish everything that decorators can (at least not without framework-specific runtime support). So, implementing decorators provides you with a more general purpose solution capable of handling much more and lessens the possibility of needing to add additional language features for new scenarios down the road.
  2. Decorators will likely become part of JavaScript rather than Annotations, which means that TypeScript, in order to remain relevant, will need to have a solution for that eventually.
  3. Decorators are more-reusable in a cross-library or cross-framework way. For example, I can write a decorator that generates all the Object.observe notifier api boilerplate for computed properties and that will be reusable across any project. But, if I had to do this with simply data annotations, I would then need an additional runtime capable of instantiating all the objects, reading the metadata and configuring the objects.

Yes, decorators is a more involved language feature to design, especially for TypeScript. But, it's probably better in the long run.

EisenbergEffect commented Jan 19, 2015

@JeroMiya You are looking at this from the perspective of C#. Those are valid use cases, but you need to consider how similar techniques were used elsewhere, like in Ruby for example. In this case similar ideas are being used to do metaprogramming which absolutely changed the definition of the object and I would argue is a key aspect of the success of the language and of frameworks like RoR.

I see a couple of important points that need to be considered for the language design:

  1. Decorators can accomplish everything that annotations can, but annotations cannot accomplish everything that decorators can (at least not without framework-specific runtime support). So, implementing decorators provides you with a more general purpose solution capable of handling much more and lessens the possibility of needing to add additional language features for new scenarios down the road.
  2. Decorators will likely become part of JavaScript rather than Annotations, which means that TypeScript, in order to remain relevant, will need to have a solution for that eventually.
  3. Decorators are more-reusable in a cross-library or cross-framework way. For example, I can write a decorator that generates all the Object.observe notifier api boilerplate for computed properties and that will be reusable across any project. But, if I had to do this with simply data annotations, I would then need an additional runtime capable of instantiating all the objects, reading the metadata and configuring the objects.

Yes, decorators is a more involved language feature to design, especially for TypeScript. But, it's probably better in the long run.

@JeroMiya

This comment has been minimized.

Show comment
Hide comment
@JeroMiya

JeroMiya Jan 19, 2015

As a clarification, I would say that I am not arguing against decorators, even if I have reservations about them. I am instead proposing annotations independently. You can consider them to be sugaring for a very common family of decorator use cases, a way to nudge frameworks towards a common standard for those use cases, and as a way to align with traceur/atscript. You could conceivably have both in the language, so long as you used a different sigil for each (@ for annotations, -/+ for decorators, for example).

Also, while you can implement annotations using decorators, the result is not as clean. For example, you can implement them one of two ways that I can think of: via an 'annotate' decorator that takes an annotation list as input:

// with annotations:
@Annotation1('arguments')
class MyClass {}

// with an +annotate decorator:
class MyClass {
 + annotate([new Annotation1('arguments')])
}

The drawback to this approach is that the syntax is painful. At this point, you might as well just use a static:

class MyClass {
  static annotate = [new Annotation1('arguments')]);
}

Alternatively, each decorator could add the annotate field internally, so you could get roughly back to the same clean declarative syntax:

class MyClass {
  + annotation1('arguments')
}

The drawback to this approach is that each decorator would need boilerplate code for adding itself to the annotate array of the constructor function, if it doesn't exist, else creating it. That is somewhat brittle, as any decorator which forgets to check if the array already exists would end up overwriting the existing one.

Using decorators to implement member and parameter annotations, however, is troublesome:

// with annotations
class MyClass {
    constructor(@ArgumentAnnotation() arg: any) {}

    @FieldAnnotation1()
    @FieldAnnotation2()
    - fieldDecorator()
    field1: string;
}

// with just decorators
class MyClass {
    // can't use a member decorator to annotate a member,
    // because we're modifying the 'properties' field of MyClass, not the member itself
    +annotateMember('field1', [new FieldAnnotation1(), new FieldAnnotation2()])
    +annotateArguments([{annotate: [new ArgumentAnnotation()]}])
    constructor(arg: any) {}

    // note how member decorators have to be split up, with annotations being next
    // to the class decorators, and field decorators next to the field.
    -fieldDecorator()
    field1: string;
}

JeroMiya commented Jan 19, 2015

As a clarification, I would say that I am not arguing against decorators, even if I have reservations about them. I am instead proposing annotations independently. You can consider them to be sugaring for a very common family of decorator use cases, a way to nudge frameworks towards a common standard for those use cases, and as a way to align with traceur/atscript. You could conceivably have both in the language, so long as you used a different sigil for each (@ for annotations, -/+ for decorators, for example).

Also, while you can implement annotations using decorators, the result is not as clean. For example, you can implement them one of two ways that I can think of: via an 'annotate' decorator that takes an annotation list as input:

// with annotations:
@Annotation1('arguments')
class MyClass {}

// with an +annotate decorator:
class MyClass {
 + annotate([new Annotation1('arguments')])
}

The drawback to this approach is that the syntax is painful. At this point, you might as well just use a static:

class MyClass {
  static annotate = [new Annotation1('arguments')]);
}

Alternatively, each decorator could add the annotate field internally, so you could get roughly back to the same clean declarative syntax:

class MyClass {
  + annotation1('arguments')
}

The drawback to this approach is that each decorator would need boilerplate code for adding itself to the annotate array of the constructor function, if it doesn't exist, else creating it. That is somewhat brittle, as any decorator which forgets to check if the array already exists would end up overwriting the existing one.

Using decorators to implement member and parameter annotations, however, is troublesome:

// with annotations
class MyClass {
    constructor(@ArgumentAnnotation() arg: any) {}

    @FieldAnnotation1()
    @FieldAnnotation2()
    - fieldDecorator()
    field1: string;
}

// with just decorators
class MyClass {
    // can't use a member decorator to annotate a member,
    // because we're modifying the 'properties' field of MyClass, not the member itself
    +annotateMember('field1', [new FieldAnnotation1(), new FieldAnnotation2()])
    +annotateArguments([{annotate: [new ArgumentAnnotation()]}])
    constructor(arg: any) {}

    // note how member decorators have to be split up, with annotations being next
    // to the class decorators, and field decorators next to the field.
    -fieldDecorator()
    field1: string;
}
@meirgottlieb

This comment has been minimized.

Show comment
Hide comment
@meirgottlieb

meirgottlieb Feb 3, 2015

Here is another approach for data-only annotations: https://github.com/artifacthealth/tsreflect-compiler.

meirgottlieb commented Feb 3, 2015

Here is another approach for data-only annotations: https://github.com/artifacthealth/tsreflect-compiler.

@RichiCoder1

This comment has been minimized.

Show comment
Hide comment
@RichiCoder1

RichiCoder1 Mar 6, 2015

So I take it this is becoming a thing in 1.5 ala. AtScript merge announcement?

RichiCoder1 commented Mar 6, 2015

So I take it this is becoming a thing in 1.5 ala. AtScript merge announcement?

@MgSam

This comment has been minimized.

Show comment
Hide comment
@MgSam

MgSam Mar 6, 2015

@msft Are you guys going to post the actual spec that you ended up implementing for this feature?

MgSam commented Mar 6, 2015

@msft Are you guys going to post the actual spec that you ended up implementing for this feature?

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Mar 6, 2015

Member

The spec will be updated, same as we do for everything else. Not sure exactly when that will be (usually the spec gets updated right before a feature starts getting implemented, i.e. early, or after the feature is implemented and we've worked out the kinks, i.e. late).

Member

RyanCavanaugh commented Mar 6, 2015

The spec will be updated, same as we do for everything else. Not sure exactly when that will be (usually the spec gets updated right before a feature starts getting implemented, i.e. early, or after the feature is implemented and we've worked out the kinks, i.e. late).

@jonathandturner

This comment has been minimized.

Show comment
Hide comment
@jonathandturner

jonathandturner Mar 7, 2015

Contributor

Not sure if it got linked already, but the draft spec @rbuckton used was here: https://github.com/jonathandturner/brainstorming

We should probably revise and post it to a more official place.

Contributor

jonathandturner commented Mar 7, 2015

Not sure if it got linked already, but the draft spec @rbuckton used was here: https://github.com/jonathandturner/brainstorming

We should probably revise and post it to a more official place.

@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Mar 7, 2015

I'm planning on extracting the salient parts into a concrete TC39 proposal this weekend and present it at the next TC39 in a few weeks.

wycats commented Mar 7, 2015

I'm planning on extracting the salient parts into a concrete TC39 proposal this weekend and present it at the next TC39 in a few weeks.

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Apr 10, 2015

Contributor

This is now tracked in #2249

Contributor

mhegazy commented Apr 10, 2015

This is now tracked in #2249

@mhegazy mhegazy closed this Apr 10, 2015

@lee-elenbaas

This comment has been minimized.

Show comment
Hide comment
@lee-elenbaas

lee-elenbaas May 5, 2015

Just a quick reminder for annotations that does cause functionality change - aspect programming, both in java and C# rely heavily on annotations, and on building annotations that deal with the aspect, and have that aspect triggered using annotations

lee-elenbaas commented May 5, 2015

Just a quick reminder for annotations that does cause functionality change - aspect programming, both in java and C# rely heavily on annotations, and on building annotations that deal with the aspect, and have that aspect triggered using annotations

@Microsoft Microsoft locked and limited conversation to collaborators Jun 18, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.