-
Notifications
You must be signed in to change notification settings - Fork 9
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 - Bridging decorator annotations with this proposal #11
Comments
Thanks for opening the issue over here @matthew-dean, appreciate it! As noted on the other issue, I think that this style of annotation could possibly be useful for the type annotations that typescript is looking to emit (cc @rbuckton) but its usage of Symbol.metadata would conflict with the other metadata system's usage of that symbol. There are many more use cases for dynamic metadata than static metadata, so if we have to choose between the two I would choose dynamic (e.g. the one outlined by in this repo). If there is a way to expose this information without the conflict though I could see it being useful. |
Dynamic metadata
I was just about to address this! I would say a few things:
function dynamic(annotation) {
return (value, context) => {
context.addInitializer(function() {
context[Symbol.metadata] = [annotation];
});
}
}
@dynamic('a')
function fn() { } In other words, with decorators themselves, if they were simply expanded to functions, variables, and values, could be used to dynamically assign values to the function meta(annotation) {
return (value, { addMeta }) => {
context.addMeta(annotation);
}
}
@meta('a')
function fn() { } Once you begin to view the metadata info as a simple object key on values, the concept becomes very powerful.
@[dynamic('a')]
function fn() { } But again, you could also do this with regular decorators, as long as decorators are just expanded to other primitive types and you allow setting the metadata key, because decorators also have this format for evaluating an expression that's done as a function call: @(dynamic('a')) // already in the Stage 3 proposal
const { metadata } = Symbol
function fn(x) {
// alter metadata at call time
fn[metadata] = [x]
} None of having statically-analyzable metadata prevents a huge number of dynamic options for metadata. So optimizing for static first doesn't at all prevent dynamic assignment (in JavaScript, it's often just objects all the way down), but optimizing for dynamic first, which this repo's proposal currently does does prevent (IMO) adoption by static-analysis engines like type-checkers, because the runtime effects (a huge increase in runtime function calls) would be considered an unacceptable trade-off. |
To clarify, the current idea for dynamic metadata is to pass in an object on function dec(_, context) {
context.metadata.foo = 'bar';
}
@dec class C {}
C[Symbol.metadata].foo; // 'bar'; Annotations would need to work with this system, e.g. by assigning themselves to a predetermined name on the metadata object. Some questions that raises:
Alternatively annotations could expose metadata in a different way, so there isn't a conflict with |
@pzuraq Oh interesting. I guess the fundamental question is: Is this metadata proposal and my proposal two different proposals? Or is there space for metadata / annotations to work together (or be combined into one proposal)? |
I would say it's moreso that this metadata proposal is currently at stage 2 and has had a lot of thought behind it and consensus being built so far. There are also a lot of use cases for it, so it seems likely that it'll move forward. Annotations could in theory be added to this proposal, but that is likely to slow it down and cause it be held up in committee. Previous feedback has always been to make the proposals smaller, that's why this was broken out into its own proposal in the first place. So, more likely, annotations would need to build on top of metadata as a separate proposal. That said, we can design metadata such that it would be possible to add annotations on top relatively seamlessly, that's how a lot of these proposals work. So we can continue the conversation, and try to figure out what that would look like. The starting point for that is taking the current design of metadata and trying to build annotations on top of it. If that's not possible, maybe we change the design of metadata - but we're going to need some compelling reasons to do so. The choices behind the metadata design are intentional and were made to solve various constraints, so we need to A. make sure it's possible to solve those same constraints with an alternative design and B. make sure that the alternate design is worthwhile. |
I guess part of what I'm trying to demonstrate, is that with the right minimal design, annotations could possibly jump in demand to one of the highest-demand proposals, if it essentially jump-starts compile-less type-checking systems. But, that said, I also get what you're saying and don't want to slow anything down.
Hmm. I think if there's a hard requirement to include the |
This iteration of the proposal came to be because the committee rejected the more complex version where a metadata object was created which had a predetermined shape. The complexity was too opinionated and seemed overengineered. My worry is that annotations would have the same problem, which is why I asked these questions:
Let's consider an annotated class: @('foo')
class C {
@('bar')
@('type:string')
publicField;
@('baz')
@('type:number')
#privateField;
@('foobar') publicMethod() {}
@('barbaz') #privateMethod() {}
} How do we expose all of these annotations to the user? It's not as simple as just adding them to We can't just add the annotations in an array on the class, because it matters what was being annotated. So there are two options:
C[Symbol.metadata] = [
{
type: 'class',
annotations: ['foo'],
},
{
type: 'field',
public: true,
name: 'publicField',
annotations: ['bar', 'type:string'],
},
{
type: 'field',
public: false,
name: 'privateField',
annotations: ['baz', 'type:number'],
},
// ...
]; One thing to note here is that the private field annotations are effectively useless, we can't actually do anything with them because we don't have access to the private field. We can fix this by adding a getter/setter for that. C[Symbol.metadata] = [
// ...
{
type: 'field',
public: false,
name: 'privateField',
access: { get, set },
annotations: ['baz', 'type:number'],
},
// ...
]; The other alternative is to build up a mapping datastructure of sorts. This is what the previous proposal for decorators did: C[Symbol.metadata] = {
own: ['foo'],
public: {
publicField: ['bar', 'type:string'],
// ...
},
private: [
{
name: 'privateField',
access: { get, set },
annotations: ['baz', 'type:number'],
},
// ...
],
}; Note that Either way, we end up with a fairly complex data structure that has to be generated just to encode all of the relevant information to the user. This is the exact same place the previous metadata proposal ran into a wall, as the committee preferred something much less complex and opinionated. The current proposal doesn't have this issue because how metadata is stored/represented is left up to the decorator author. I recognize that there would be some advantages to a more "static" version of decorator metadata, but I think these issues would make it very hard to get through committee, plus as noted before it's considerably less expressive than the current metadata proposal. So between those two things, I'm inclined to stick with the current proposal, and annotations can be explored as a potential future addition. |
TLDR: https://github.com/tc39/proposal-type-annotations seems better to me. But I also don't like them being ignorable. Can we fork that proposal and make them non-ignorable. Actual type checking would be awesome (but would have a cost). Or, at least, it would be possible for type annotations to expose I feel like the annotations would be a really ugly language feature. This, import {int, string} from 'some-runtime-type-lib'
function foo(@string name, @int num) {
// ...
} is much cleaner than this, import {check} from 'some-runtime-type-lib'
function foo(@'string' name, @'int' num) {
check(foo, name, num)
// ...
} or this, import {check} from 'some-runtime-type-lib'
function foo(@{type: 'string'} name, @{type: 'int'} num) {
check(foo, name, num)
// ...
} . Decorators are already annotations, and they already accept data if they are decorator factory functions. Having all this extra noise, f.e. @{foo: "one"} @{bar: "two"} class C {
@{lorem: 123} method() { }
@{ipsum: 456} field;
} is really really noisy and to me is very ugly. It seems to me the main motivation for this feature so far is type checking. Why introduce such ugly type syntax? Why don't we work on the type annotation proposal instead of making a new syntax that TypeScript and Flow both do not support at all yet, that would be completely redundant, and that would be inconvenient to type on keyboard compared to what we already have? |
I'm not personally swayed by a If you want arbitrary Metadata, you could still do this instead: @((_, c) => c.metadata.foo = "bar")
class C {} Which could just as easily be a reusable |
Hi, I was sent this way from tc39/proposal-decorators#489 because the decorators proposal has an extensions page that discusses annotations that I believe could be more suitable for metadata.
I don't have any objections to this (repo's) proposal, per se, but I think one of the thing that the extensions annotations proposal and my subsequent proposal based on that does well is make the runtime effects of annotations very minimal.
This makes those proposals more suitable for type inference / type-checking systems, because no functions are (usually) executed at runtime, making them ideal for static analysis, whereas this proposal has a larger runtime cost. So while this current
proposal-decorator-metadata
is not suitable as a static typing system for TypeScript / Flow, what I'm proposing would absolutely be suitable for static type-checking systems.Also, the annotations proposal has a much tighter syntax than this metadata proposal, making the developer ergonomics more attractive for replacement of type-checking systems.
Please take a look at let me know if there's an opportunity to merge these proposals. -- https://github.com/matthew-dean/proposal-annotations
Example
Just as an example, this repo's proposal adds metadata like:
My proposal takes the much more concise extensions on the decorators proposal and turns the above example into this form:
That's a reduction of ~250 characters to ~80 characters.
Where this syntax really shines is as a replacement for the controversial type-annotations proposal. Instead of adding a bunch of "ignorable" syntax to JS, it adds a single annotation syntax (
@''
,@[]
,@{}
), with versatility to use strings, arrays, and objects to mean whatever a system might want it to mean.Some examples from the proposal.
This would supplant the need to add
type
blocks,interface
blocks, generics, type assignment, type assertions, non-nullable assertions etc etc etc to the JavaScript language as "comments".Instead, this metadata approach, because it has very little runtime effect (it does not need to execute a function to decorate with simple primitives), could supply all TypeScript / Flow requirements within JavaScript, as well as preserve those type (or whatever kind) of annotations at runtime, making writing custom runtime helpers trivial.
Thanks for considering this!
The text was updated successfully, but these errors were encountered: