-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Comments
My 2c: as they're defined in AtScript now, annotations have a couple of nasty problems
Angular, Ember and other frameworks already do this kind of wrapping (e.g. see 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. |
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. |
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.
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)
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 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... |
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. |
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. |
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.
|
@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 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 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. |
@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.) |
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. |
Yes, exactly. Decorators provide a strict superset of the capabilities of metadata annotations. |
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 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? |
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. |
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 Yes I am championing this feature for ES7 and would love help. Email me? (wycats@gmail.com) |
@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); |
As I said to the TypeScript team, I think the exact opposite of this is true.
For example, consider a hypothetical function nonconfigurable(prototype, name, descriptor) {
descriptor.configurable = false;
return descriptor;
} And this usage: class Article {
@nonconfigurable
length() { /* implementation */ }
} In this case, the You could imagine TypeScript providing a way for the 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. |
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. |
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 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:
Yes, decorators is a more involved language feature to design, especially for TypeScript. But, it's probably better in the long run. |
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;
} |
Here is another approach for data-only annotations: https://github.com/artifacthealth/tsreflect-compiler. |
So I take it this is becoming a thing in 1.5 ala. AtScript merge announcement? |
@msft Are you guys going to post the actual spec that you ended up implementing for this feature? |
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). |
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. |
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. |
This is now tracked in #2249 |
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 |
Summary
Prior Art/Reference
Motivation
Differences from AtScript/Traceur
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:
A class:
A constructor of a class:
A field of a class:
Arguments to a function:
Extensions to lib.d.ts to support typing of annotations
The text was updated successfully, but these errors were encountered: