GithubHelp home page GithubHelp logo

On `extras` about proposal-decorators HOT 12 CLOSED

tc39 avatar tc39 commented on August 16, 2024
On `extras`

from proposal-decorators.

Comments (12)

littledan avatar littledan commented on August 16, 2024

This is a good point about duplicates. Previously, there was some (deliberately) vague text about merging get/set pairs for duplicates, but it was left open for future discussion how to handle overlaps. I wasn't convinced that we need the merging, so I took it out, but I forgot to add in logic to handle overlaps. I was thinking, maybe this should just be a ReferenceError when the decorators run. What sort of error recovery would you install otherwise?

You can remove properties using a class decorator, but I don't know if that meets your use cases. Can you explain more concretely what your use case is? In a class decorator, you can also test the elements to see what are missing, and add one conditionally.

About removing values: I think that would be a reasonable feature, for example return undefined from the decorator to just delete the element. OTOH, it's hard for me to see how you can make an element decorator transmit information to other ones, as it's not given the identity of anything shared to put extra properties on.

from proposal-decorators.

uhop avatar uhop commented on August 16, 2024

What sort of error recovery would you install otherwise?

I would leave it up to a user, with a reasonable default. A user (actually a class decorator) can decide to combine things using various algorithms (AOP weaving, method chaining), or override things (simple OOP-like last one wins), or warn a user (traits). The default can be as simple as an override.

You can remove properties using a class decorator, but I don't know if that meets your use cases. Can you explain more concretely what your use case is?

Use cases are good mostly for designing applications, less so for libraries, and even less so for standards. Even if we collect 100 reasonable use cases, it doesn't guarantee that down the road people would not find/desire a different way to use it. In this case redesign of the standard can be costly.

In case when we talk about a definitive data model, it makes sense to follow the mathematical completeness. In the proposal the model is a collection of descriptors, which can be applied on a prototype, on a constructor, or on an instance while constructing. Each descriptor has a unique name, but no apparent order, so we are talking about a dictionary (like for Object.defineProperties()). In order to be complete we should be able to modify those objects by adding/removing elements (name/descriptor pair), and inspect them by enumerating. I believe we have all of those operations, but removing.

OTOH, it can be argued that a class decorator can remove things while enumerating using existing means. Yet, usually I go for a simple completeness, if it is not taxing to add. By taxing, I mean both potential performance penalties, and a logical complexity. For example your proposal looks simple, yet effective from this point of view:

About removing values: I think that would be a reasonable feature, for example return undefined from the decorator to just delete the element.

But let's go back a bit:

In a class decorator, you can also test the elements to see what are missing, and add one conditionally.

Yes. There are two ways to do it:

  1. Intractable way: every time I need something, I write a custom decorator to check, if something missing (or present).
  2. Better way: record a meta-information about a property, then act on it at a class level.

Here go my use cases you asked for above.

In fact, the latter is how AOP works (simplified):

  • A method is annotated as having a before/after advice (or both).
  • When constructor is being created:
    • All AOP annotated methods down the prototype/inheritance chain are identified through a metadata.
    • All advices are collected.
    • All AOP annotated methods are replaced with a stub:
      function () {
        // runs the before advices down the prototype chain
      
        // runs the original method, which in turn can call super
        const result = theOriginalMethod.apply(this, arguments);
      
        // runs the after advices up the prototype chain
      
        return result;
      }
  • If the constructor was annotated, proxy it, and run the "after" chain using the ES6 proxy.
  • It should be possible to annotate a method with advices, which is not defined yet.
    • It can be defined by a parent in a prototype chain.
    • Or it can be defined later by a derived class.

The same way a method chaining can be done. The method chaining runs procedures (functions that do not return meaningful values) down or up a prototype chain — quite similar to AOP advice chains. That one is simple enough and virtually used by all OOP languages including JS:

  • Constructors are chained using "after" algorithm: the base class constructor runs before the derived constructor, and so on.
    • JS requires to call super(), yet enforces it, if you don't do that. Effectively it forces programmers to chain manually in the "after" fashion.
  • Destructors (like in C++) are chained using "before" algorithm, which runs opposite to constructors.

Both are useful for so-called life-cycle methods popular in modern frameworks (React, Angular, ...), browsers (Web Components, Service Workers, ...), and so on. In a sense, constructors and destructors are specialized life-cycle methods.

While chaining can be simulated by calling super, unlike constructors it is not enforced for any other life-cycle methods, and it is easy to make a mistake. Additionally it consumes stack — frequently even TCO cannot help here.

With decorators it can be done like that (simplified):

  • Mark a method (or null, if we don't supply a method) as chainable.
  • When constructor is being created:
    • Retrieve all annotated methods.
    • Collect all methods down the prototype chain.
    • Replace a method with a stub:
      function () {
        // reverse chain for "after" chaining
        chain.forEach(method => { method.apply(this, arguments); });
      }

As you can see this kind of stubbing do not abuse the stack.

In general, stack or not, there are two misconceptions:

  1. All decorators can be implemented by wrapping a function right there, right now — not true.
    • Quite frequent mechanism is to mark (with a metadata) and delay the action until later (e.g., a class creation).
  2. All ways to combine methods can be done with supernot true.
    • AOP cannot be done with only super.
    • Again a metadata will be involved.

Actually the metadata case is so pervasive that it has a special mention in TypeScript, which provides decorators for some time: https://www.typescriptlang.org/docs/handbook/decorators.html#metadata — apparently it is being proposed as well. :-)

To wit: https://github.com/rbuckton/reflect-metadata

Personally I am not that convinced that we need a formal metadata defined, but we do need a way to communicate an arbitrary information from low-level property decorators up to a class decorator.

OTOH, it's hard for me to see how you can make an element decorator transmit information to other ones, as it's not given the identity of anything shared to put extra properties on.

Precisely. I think it should be solved.

PS: Right now I record metadata directly on the prototype or on the constructor function. My "decorator" (essentially a wrapper) knows where to look, and what to look for in order to complete a class.

from proposal-decorators.

littledan avatar littledan commented on August 16, 2024

Use cases are good mostly for designing applications, less so for libraries, and even less so for standards.

Well, when I was saying "use cases", I didn't mean "here's how to use decorators to improve the clickthrough rate", but more like, here's the kind of decorator that you'd want to write with this language feature. (Just like when I might say "user" I mean JavaScript programmers.) So, thanks for explaining use cases such as custom element lifecycle callbacks.

The same way a method chaining can be done. The method chaining runs procedures (functions that do not return meaningful values) down or up a prototype chain — quite similar to AOP advice chains.

I believe such chains could be installed on a method with a particular name pretty straightforwardly with a class decorator, right?

Personally I am not that convinced that we need a formal metadata defined, but we do need a way to communicate an arbitrary information from low-level property decorators up to a class decorator.

OK, I'm pretty convinced by your examples. You could add expandos on the method/initializer value itself to communicate information, but that's a bit scummy-feeling and leaks to users.

As a strawman, what if element descriptors had a "metadata" property, which was initially undefined, but which users could fill in, and which would be propagated to later decorators? Users would be expected to apply a convention that's something like this in order to let things compose:

  descriptor.metadata = descriptor.metadata || {};
  descriptor.metadata[mySymbol] = ...;

Alternatively, we could give the same descriptor object to everyone, rather than making them single-use, constantly generating new ones; users could just add whatever properties they want to those objects.

Quite frequent mechanism is to mark (with a metadata) and delay the action until later (e.g., a class creation).

If we solve the metadata issue: Is it OK that, in the current proposal, these "later actions" would need a class decorator to trigger them?

from proposal-decorators.

uhop avatar uhop commented on August 16, 2024

I believe such chains could be installed on a method with a particular name pretty straightforwardly with a class decorator, right?

Context: chaining.

Not really. Not straightforward. The simplest way is in my example in the previous post.

I think you are imagining something like that (using fn(name, fn) decorator model for simplicity):

const chainAfterDecorator = (name, fn) => (
  (...args) => {
    super[name](...args);
    fn.apply(this, args);
  }
);

The problem with this approach is that super is bound statically, and it will not pick up true super outside of a class context. Another potential problem is that the very first method does not have a super, so we need to add a test for it further complicating the simple stub, and doing a dynamic test every call, when we know things statically. And we can go deep in the stack, which is never good.

To sum it up: a simple stub with a loop will probably be faster, simpler, and easier to debug, if the need arises.

OK, I'm pretty convinced by your examples. You could add expandos on the method/initializer value itself to communicate information, but that's a bit scummy-feeling and leaks to users.

I like your strawman, but I imagined it a little bit simpler. Basically I imagined that all decorators will receive an extra parameter (they can ignore it completely in trivial cases), which is a simple object. This object is guaranteed to be the same for all decorators that are being applied for a given instance of a class. Something like that:

const chaining = Symbol('chaining');

// class decorator
export const enforceChaining = (descriptor, meta) => {
  if (meta[chaining]) {
    // iterate over the map, chaining methods using different styles
  }
};

// method decorator
const chainingDecorator = style => (
  (descriptor, meta) => {
    const chainMap = meta[chaining];
    if (chainMap) {
      if (chainMap.has(descriptor.name)) {
        if (chainMap.get(descriptor.name) !== style) {
          throw new Error('contradicting chaining instructions');
        }
      } else {
        chainMap.set(descriptor.name, style);
      }
    } else {
      meta[chaining] = new Map([[descriptor.name, style]]);
    }
  }
);

// the implementation above ignores chaining instructions set in base classes
// purely for simplicity.

export const chainBefore = chainingDecorator('before');
export const chainAfter  = chainingDecorator('after');

Admittedly it is not that far from your strawman — both approaches are functionally identical.

One problem with both approaches is a presence of an elaborate setup in every low-level descriptor. In the example above:

  • In a method decorator on every invocation:
    • I check that a required data structure exists.
    • If it is not there, I recreate it. The more complex the structure is, the more complex this step.
      • I took an opportunity to optimize creating a structure + filling it with the initial data.
    • Otherwise I do something with it.
  • In a class decorator:
    • I check that a required data structure exists.
    • If it does, I do my stuff, otherwise I may skip my actions.
      • I can still inspect my class to make sure that I don't need to do something required by decorators from my base classes.

Decorators can be simplified, if we (users, or likely class decorators) had an opportunity to create data structures we need before all other decorators are run. Maybe calling a class decorator with a special parameter, so it can set up meta for others, then calling it again, when a class is formed.

Obviously it is not something insurmountable, but something to think about.

Another possible problem is already mentioned: what if I need a meta from my base classes? The simple way to solve it is to save the meta on a constructor (as a static member) using a symbol. Derived classes can inspect it, and take appropriate actions.

Possible question: what if I don't want to save meta for my decorator, because it has a transient nature? Storing stuff we don't need is an unnecessary load on memory. Possible answer: a class decorator can delete all unnecessary data, when it is run.

If this functionality is not provided, it will be done ad-hoc by some decorators. So if it is not there, I don't think people will revolt.

If we solve the metadata issue: Is it OK that, in the current proposal, these "later actions" would need a class decorator to trigger them?

I think this question (like two above: "initialize meta" and "save meta for derived classes") belongs to the category "nice to have". It is totally OK to ask a user to specify a class decorator manually. The worse thing that can happen is non-working decorators without alerting a user.

The "nice" solution would allow a property decorator to detect that there is no appropriate class decorator (by meta already set up as discussed above?), and attach the default one, if needed and when appropriate, or complain to a user.

from proposal-decorators.

littledan avatar littledan commented on August 16, 2024

The problem with this approach is that super is bound statically, and it will not pick up true super outside of a class context.
To sum it up: a simple stub with a loop will probably be faster, simpler, and easier to debug, if the need arises.

Sorry, I don't really know what you're getting at here. What do you want to loop over? I thought you'd loop over the prototype chain. super dynamically looks at whatever is the current the prototype of the home object of the method. Are you thinking about re-targeting methods to other classes here? Or not being based on the prototype chain?

I can't really make out what's going on in your chaining example: how can you implement before/after functionality by iterating over the map if the meta is per-class or per-element--don't you want to interact with superclasses at some point?

Overall, it'd be easier for me to understand if you could give concrete examples of, "this is the kind of decorator I'd like to write with before/after chaining, and here's what happens when wrapping the method doesn't work."

I like your strawman, but I imagined it a little bit simpler. Basically I imagined that all decorators will receive an extra parameter (they can ignore it completely in trivial cases), which is a simple object.

One reason for the design I mentioned was that, in your design, this object has to be allocated even if none of the decorators will use it, whereas in my design, undefined can simply be passed around. If it's only something that you can go in and mutate, without passing out a new value, it's less "functional", as well. Anyway, I agree that they're basically the same; this is a tiny detail.

Decorators can be simplified, if we (users, or likely class decorators) had an opportunity to create data structures we need before all other decorators are run.

Interesting idea. We already have a bunch of stages, and I can imagine other reasons for adding more later (e.g., a macro expansion stage, before any of this). I can see a reason for this, but I don't want the complexity to get out of hand. I like your idea of storing metadata in Symbol-keyed properties.

The worse thing that can happen is non-working decorators without alerting a user.

Hmm, right now, property decorators are deliberately scoped to not get anything relating to the whole class. However, I can see how this would be useful.

cc @bterlson @wycats

from proposal-decorators.

uhop avatar uhop commented on August 16, 2024

Sorry, I don't really know what you're getting at here.

I am not sure how you are up on AOP concepts, so pardon, if I oversimplify things. At the end of the day AOP can identify events, and run advices at those events. The nature of those advices is immaterial right now, but they can access an information about the event, and affect it.

Classically there are three types of advices (functions): before, around, and after. "Around" advice is more-or-less a super call: it is called on event, and at any point it can invoke an original handler, or ignore it completely. When it calls the original handler, it can be another "around" advice, and so on. "Before" advice runs before the event, usually setting a stage. And "after" advice runs after the event.

In JS not a lot of things are interceptable, but the most important join point is a method call. In ES6, with its super, we don't need an "around" advice. But we do need "before" and "after" advices.

A little bit of code to illustrate the concept and the problem (for simplicity I omit arguments of advices).

First take (very naive, and incorrect, but frequently simulated in the wild):

class A {
  before_m1() { console.log('A::before_m1'); }
  after_m1() { console.log('A::after_m1'); }
  m1() {
    this.before_m1(); // simulating before
    console.log('A::m1');
    this.after_m1();  // simulating after
  }
}

class B extends A {
  before_m1() { console.log('B::before_m1'); }
  after_m1() { console.log('B::after_m1'); }
  m1() {
    this.before_m1(); // simulating before
    super.m1();       // simulating around
    console.log('B::m1');
    this.after_m1();  // simulating after
  }
}

new A().m1(); // prints (correctly):
// A::before_m1
// A::m1
// A::after_m1

new B().m1(); // prints (incorrectly):
// B::before_m1
// B::before_m1
// A::m1
// B::after_m1
// B::m1
// B::after_m1

Two problems right there:

  1. All "before" and "after" advices of A are not called.
    • Can be fixed by using stand-alone functions, or methods with unique names.
  2. Our B "around" advice is between two "after" advices. It should be: all "before" advices, then all "around" advices (essentially our main methods), then all "after" advices.

Let's fix the first problem:

class A {
  a_before_m1() { console.log('A::before_m1'); }
  a_after_m1() { console.log('A::after_m1'); }
  m1() {
    this.a_before_m1(); // simulating before
    console.log('A::m1');
    this.a_after_m1();  // simulating after
  }
}

class B extends A {
  b_before_m1() { console.log('B::before_m1'); }
  b_after_m1() { console.log('B::after_m1'); }
  m1() {
    this.b_before_m1(); // simulating before
    super.m1();       // simulating around
    console.log('B::m1');
    this.b_after_m1();  // simulating after
  }
}

class C extends A {
  c_before_m1() { console.log('C::before_m1'); }
  c_after_m1() { console.log('C::after_m1'); }
  m1() {
    this.c_before_m1(); // simulating before
    console.log('C::m1');
    this.c_after_m1();  // simulating after
  }
}

new A().m1(); // prints (correctly):
// A::before_m1
// A::m1
// A::after_m1

new B().m1(); // prints (incorrectly):
// B::before_m1
// A::before_m1
// A::m1
// A::after_m1
// B::m1
// B::after_m1

new C().m1(); // prints (incorrectly):
// C::before_m1
// C::m1
// C::after_m1

Now the first problem of B is fixed and we call the correct advices. Yet we still have the second problem: the "after" advice of A is called before the main body (the "around" advice) of B::m1().

C demonstrates the third problem: if my "around" advice decided not to call its super, so no advices of base classes are called. All "before" and "after" advices should be called always regardless what an "around" advice does.

So realistically the top method is going to be a generated one. I'll write a pseudo code from the top of my head to demonstrate a potential solution assuming that method decorators are called with signature fn(name, value, meta), and class decorators are called as fn(ctr, meta):

function stub (original, beforeChain, afterChain) {
  return function () {
    for (let i = 0; i < beforeChain.length; ++i) {
      beforeChain[i].apply(this, arguments);
    }
    const result = original.apply(this, arguments);
    for (let i = aroundChain - 1; i >= 0; --i) {
      aroundChain[i].call(this, arguments, result);
    }
    return result;
  };
}

const nop = () => {};

const iterateOverPrototypes = (proto, callback) => {
  while (proto && proto !== Object.prototype) {
    callback(proto);
    proto = Object.getPrototypeOf(proto);
  }
};

const collectAdvices = (proto, name) => {
  let beforeChain = [], afterChain = [];
  iterateOverPrototypes(proto, proto => {
    const ctr = proto.constructor;
    const advices = ctr.meta && ctr.meta[name];
    if (advices) {
      advices.before && beforeChain.push(advices.before);
      advices.after  && afterChain.push(advices.after);
    }
  });
  return {beforeChain, afterChain};
};

const collectAdvisedMethods = proto => {
  let names = new Set();
  iterateOverPrototypes(proto, proto => {
    const ctr = proto.constructor;
    if (ctr.meta) {
      Object.keys(ctr.meta).forEach(name => names.add(name));
    }
  });
  return names;
};

// decorators

const adviseClass = (ctr, meta) => {
  // uber simple implementation, corners cut left and right
  ctr.originals = {};
  collectAdvisedNames(proto).forEach(name => {
    const original = ctr.originals && ctr.originals[name] || ctr.prototype[name] || nop;
    const advices = collectAdvices(ctr.prototype, name);
    ctr.prototype[name] = stub(original, advices.beforeChain, advices.afterChain);
    ctr.originals[name] = original;
  });
};

const advise = (before, after) => (
  (name, value, meta) => {
    if (!meta[name]) {
      meta[name] = {beforeChain: [], afterChain: []};
    }
    if (before) {
      meta[name].beforeChain.push(before);
    }
    if (after) {
      meta[name].beforeChain.push(after);
    }
  }
);

With setup like that I can rewrite my last example:

@adviceClass
class A {
  @advise(() => { console.log('A::before_m1'); }, () => { console.log('A::after_m1'); })
  m1() { console.log('A::m1'); }
}

@adviceClass
class B extends A {
  @advise(() => { console.log('B::before_m1'); }, () => { console.log('B::after_m1'); })
  m1() { super.m1(); console.log('B::m1'); }
}

@adviceClass
class C extends A {
  @advise(() => { console.log('C::before_m1'); }, () => { console.log('C::after_m1'); })
  m1() { console.log('C::m1'); }
}

@adviceClass
class D extends A {
  m1() { console.log('D::m1'); }
}

If I didn't mess up my code they should print (correctly this time):

new A().m1(); // prints (correctly):
// A::before_m1
// A::m1
// A::after_m1

new B().m1(); // prints (correctly):
// B::before_m1
// A::before_m1
// A::m1
// B::m1
// A::after_m1
// B::after_m1

new C().m1(); // prints (correctly):
// C::before_m1
// A::before_m1
// C::m1
// A::after_m1
// C::after_m1

new D().m1(); // prints (correctly):
// A::before_m1
// D::m1
// A::after_m1

Actually all this stuff (AOP and chaining) was implemented for years in dcl. Version 1.x is pre ES5, and used for ~10 years. Version 2.x is for ES6 and can handle descriptors ⇒ it supports advising getters and setters. Obviously none of them use decorators, just whatever is available in ES5. I hope to reduce their code base by removing all complicated inspections of prototypes, methods, descriptors and so on with normal decorators.

PS: I have a project (close-sourced at the moment, will be published soon) called dcl6 — essentially it is dcl made specially for ES6. I hope some day it will be implemented with decorators cutting down on the codebase even more.

from proposal-decorators.

uhop avatar uhop commented on August 16, 2024

Overall, it'd be easier for me to understand if you could give concrete examples of, "this is the kind of decorator I'd like to write with before/after chaining, and here's what happens when wrapping the method doesn't work."

I hope I addressed it in the previous post. While I didn't try to run the code I wrote :-), I hope the intent is clear. Please do not hesitate to ask/criticize — I think now is the right time for a brainstorm.

...Anyway, I agree that they're basically the same; this is a tiny detail.

There is another difference in semantics: how to treat meta or descriptor.metadata. In my design it is an object, which is the same for all invocations of decorators of a given class. I am not so sure about descriptor.metadata:

  • If I assign a value to it in one decorator, is it going to be pre-assigned to another descriptor passed to another decorator?
  • If it is the same, does it mean that I can override it completely (by design or by mistake) disregarding the previous content?
  • If it is not the same, I assume it will be copied somewhere? If it is, how exactly? What is the semantics of it?

In my design I hope that decorators will use symbols, which cannot clash by design, and access only their personal part of meta. If I want to open it to other decorators, I will publish my symbol. In any case all access is intentional, and nobody can override/delete parts they have no idea about.

I can see a reason for this, but I don't want the complexity to get out of hand.

I am 100% with you on that. Nobody wants an incomprehensible monstrosity. The API, and its potential implementation should be as simple as possible. Additionally, the implementation should be as fast as possible and consume as little resources (e.g., memory) as possible.

On top of that I am a big proponent of "don't pay for things you don't use". So for simple cases it should have a small constant cost, ideally 0.

But coming back to stages, if it helps to solve some other problems — I am all for it. I would love to learn those.

from proposal-decorators.

uhop avatar uhop commented on August 16, 2024

In general I had a first-hand experience with following types of technics falling into "decorators" realm.

Pure method/property decorators

The first one is the simplest variety "decorator is fn(fn)": a function that is applied to a method. Examples:

  • debounce and throttle decorators — a time-controlled access to a method, so it is not called too frequently.
  • memoize — caches the result of a method to speed up calculations.
  • Debugging helpers:
    • trace — logs a message when a method is invoked.
    • counter — counts how many times a method is invoked.
    • timer — times a method, and calculate stats.

Decorators in this group are largely self-contained, but can cooperate with user's code, and between each other. Examples:

  • counter has an API to reset it, and to get its current value.
  • memoize has an API to reset its cache, and usually has a counterpart decorator, which resets cache automatically, when a mutating method is called.

They usually do not require a class decorator to cooperate.

Pure class decorators

The second one is on the other side of the specter of "simple": pure class decorators, which do not cooperate with others. Usually they work "by convention" deriving metadata from existing data. Examples:

  • Event-handling class decorator, which can be used for web components (or frameworks). It enumerates over methods, and if their names are in the form of "onClick" (e.g., /^on[A-Z]\w+$/), it attaches them as event handlers automagicaly to the root:
    this.addEventListener('click', this.onClick.bind(this));
    Or it can be included in the event machinery used by a framework. The same can be done with life-cycle events.
  • Intelligent stub-generating. Again, useful for web components to reflect custom attributes into custom properties. The class decorator takes a list of required property names, and if there is no getter for it in the prototype, it can create a getter, which reads value from an attribute, and possibly a setter, which propagates it back. The cadillac solution can even do a type conversion, if desired.

Cooperating property/class decorators

The third one is already described: property decorators, and class decorators interact using a metadata. Additionally they may inspect metadata left in base classes. Good examples are AOP and chaining.

More complex examples

There is one more type of decorators, which can be implemented as either a data property decorator, or a class decorator. One example would be a code generation from a domain-specific language (e.g., encoded as JSON), which is read, the descriptor for the property is suppressed, but a set of methods is generated. For example: https://github.com/heya/state/wiki#example — imagine that states is a decorated data property, and host (without user-defined callbacks) is a set of generated methods corresponding to the description (a set of strings in this case). (Look at the full doc, which explains the concepts at the beginning).

Summary

Ideally I want something simple, which at the very least will cover all use cases listed above. All of them are not theoretical, but actual code implemented without decorators, but hopefully will benefit from them by reducing the codebase, and its complexity.

from proposal-decorators.

rbuckton avatar rbuckton commented on August 16, 2024

While I would love to have a definitive mechanism for defining metadata, there are some caveats to keep in mind:

  • Metadata must be reachable both at declaration time (e.g. by decorators), and at runtime.
  • Metadata declared on a superclass must be reachable by a subclass both at declaration time and at runtime.
  • You must be able to differentiate metadata between same-named members with different placements (e.g. static, prototype, own).

We've had a number of meetings and discussions with Angular and other projects about these needs, which is why I wrote https://github.com/rbuckton/reflect-metadata to begin with.

Using a "finisher" can accomplish this readily:

function Type(type) {
  return function (member) {
    member.finisher = (klass) => {
      // read: Reflect.getMetadata("design:type", klass.prototype, member.name);
      // read own: Reflect.getOwnMetadata("design:type", klass.prototype, member.name);
      Reflect.defineMetadata("design:type", type, klass.prototype, member.name);
    };
    return member;
  }
}

If meta is passed to the decorator, then it must be structured in some fashion to provide the same capabilities:

function Type(type) {
  return function (member, meta) {
    // read? Maybe something like: meta.__proto__[member.placement][member.name]["design:type"]
    // read own: meta[member.placement][member.name]["design:type"]
    // may need to guard each element access?
    meta[member.placement][member.name]["design:type"] = type;
    return member;
  }
}

If metadata is present on the descriptor, the structure is less complicated, but you cannot read metadata from other members or the superclass:

function Type(type) {
  return function (member) {
    // read? 
    // read own: member.metadata["design:type"]
    // may need to guard/initialize metadata?
    member.metadata["design:type"] = type;
    return member;
  }
}

Generally, I think I'm in favor of both a metadata property as well as using a finisher. Decorators can then easily assign metadata specific to their member with the metadata property, and can leverage a finisher for more complex tasks.

Regardless, metadata without a reflection API to read at runtime is likely a non-starter.

from proposal-decorators.

uhop avatar uhop commented on August 16, 2024

Regardless, metadata without a reflection API to read at runtime is likely a non-starter.

Could you elaborate on that?

Even in ES5 all objects can be inspected, names, symbols, and corresponding descriptors can be extracted and manipulated. Granted, the API is far from comfortable, yet it looks like it is enough to implement various metadata schemes. How do you want to improve what we have? Isn't it orthogonal to decorators?

BTW, I looked at reflect-metadata before and was impressed that it has ~1,700 lines of code. The main file of my project dcl implements AOP + chaining + metadata required for that and it is stands at whopping ~700 lines. dcl6 (reimplementation of dcl for ES6) is much smaller. Granted that my metadata is not generic, it tells me, respectfully, that reflect-metadata is a tad on a hefty side, which probably means that the scope of API is too broad. I am happy to be convinced otherwise.

from proposal-decorators.

uhop avatar uhop commented on August 16, 2024

While I would love to have a definitive mechanism for defining metadata, there are some caveats to keep in mind:

BTW, I am totally agree with Ron's list of caveats. It sounds reasonable and born of practice. I can even back it up with examples, if needed.

from proposal-decorators.

littledan avatar littledan commented on August 16, 2024

I discussed this issue with @wycats. Leaving out metadata was intentional. The idea is that you can key any metadata off of method identities. For example, this could take place in a WeakMap. How would this be, for an initial solution?

from proposal-decorators.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.