This is building upon #23 and #61/#65. This can be combined with #66. And it might also be a provable solution for #68. See also #67 where I try to establish a common nomenclature of what types of functions/objects we are dealing with.
Here @lukescott presented a provable solution based on a suggestion made by @jayphelps that would require introduction of another built-in object that will be passed to the decorator function upon decoration.
With the introduction of the yield statement and along with it generators, two new built-in objects were introduced to the language, namely Generator and GeneratorFunction, not to mention the newly introduced Symbol.
So what about adding yet another built-in object, say
interface DecorationDescriptor
constructor(target : Class | PrototypeObject, optional attr : String, optional descriptor : DescriptorObject)
readwrite property target : Class | PrototypeObject
immutable attr : String
readwrite property descriptor : DescriptorObject
where immutable means that the property is not configurable and readonly and
readwrite means that the property is not configurable but can be written to.
Additional properties for testing the 'nature' of the descriptor, say decoratesClass : Boolean, could be added either by default or simply by extending the native object's prototype.
And, similar to Symbol, it should not be instantiated using new but rather by calling DecorationDescriptor(target, attr, descriptor), to "prevent" the casual user from using it.
Instances of this would then be passed to the decorator function upon decoration which then could use a simple instanceof check to determine whether it is being used as a decorator or whether it was called with a different set of arguments, for example in cases where we have parameterizable decorators that will return the actual decorator when being called.
Having this built-in object, will reduce the signature of the decorator function to just
function (descriptor : DecorationDescriptor) : DecorationDescriptor | void(0)
and will help eliminate the ambiguity that goes along with testing for typeof arguments[0] == 'function' or typeof arguments[0] == 'object' and so on from a decorator developer's perspective, considering the sort-of proposal found in #23.
Existing transpilers, or even native engine code, would then need to create a new instance of proposed decoration descriptor, whatever the built-in object's final name would be, and pass it to the decorator function upon decoration.
All of this, of course, goes hand in hand with the proposal made in both #23 and #61, namely eliminate the need for having to use a call statement when not requiring any additional parameters for an otherwise parameterizable decorator, e.g.
function pdecorator(options)
{
// handle options
return function decoratorImpl(target, attr, descriptor) {....};
}
would become (simplified example here!)
function pdecorator(optionsOrDescriptor)
{
let descriptor;
let options = optionsOrDescriptor;
if (optionsOrDescriptor instanceof DecorationDescriptor)
{
descriptor = optionsOrDescriptor;
options = makeDefaultOptions({});
}
else
{
options = mergeUserOptions(options);
}
const decoratorImpl = function _decoratorImpl(descriptor)
{
// make use of options here...
}
if (descriptor instanceof DecorationDescriptor)
{
return decoratorImpl(descriptor);
}
return decoratorImpl;
}
With that in place, the decorator could then be used as follows
Class Decorator Use
1. @pdecorator
class Foo {}
2. @pdecorator()
class Foo {}
3. @pdecorator({option1:'Bar'})
class Foo {}
Desugaring
1. var Foo = (function () {
function Foo() {
}
var cdd = DecorationDescriptor(Foo);
var cdd2 = pdecorator(classDecorationDescriptor) | classDecorationDescriptor;
var F = cdd2.target;
return F;
})();
2. var Foo = (function () {
function Foo() {
}
var cdd = DecorationDescriptor(Foo);
var cdd2 = pdecorator()(classDecorationDescriptor) | classDecorationDescriptor;
var F = cdd2.target;
return F;
})();
3. var Foo = (function () {
function Foo() {
}
var cdd = DecorationDescriptor(Foo);
var cdd2 = pdecorator({option1:'Bar'})(classDecorationDescriptor) | classDecorationDescriptor;
var F = cdd2.target;
return F;
})();
and so on.
Considering #68, the generated code can now safely assume that the decorator function returned an instance of DecorationDescriptor or did not have any return result.
The latter of course implies that the decorator is able to modify the descriptor in place, given the above interface proposal.
Caveats
Misuse
Of course, this will fail if the user decides to pass in a custom instance of DecorationDescriptor, e.g.
@pdecorator(DecorationDescriptor(function () {}))
class Foo {}
But then again, nothing is safe from being misused. However, this might also be a nice feature to have for frameworks that need to augment existing classes after that they have been declared.
Breaking Change
Existing decorators need to be reimplemented.
Increased Complexity of the Decorator
Decorators, even those that are not parameterizable, need to implement additional guards and also need to be implemented similarly to those that are parameterizable. But I do not see anything wrong in that, e.g.
function simpledecorator(descriptor)
{
const decoratorImpl = function _decoratorImpl(descriptor) {};
if(descriptor instanceof DecorationDescriptor)
{
// decorate ...
return decoratorImpl(descriptor);
}
return decoratorImpl;
}
and its use
@simpledecorator()
class Foo {
@simpledecorator
get prop() {}
@simpledecorator()
doSomething() {}
}
would be streamlined with that of parameterizable decorators. And, if the author of the simpledecorator decides to make it a parameterizable one, there will be no change in behavior unless these parameters are mandatory. So win-win for everyone, or is it not?
Increased Complexity in Transpiler / Engine
The engine needs to implement a new built-in object. Given that engines do not provide for such, the transpiler needs to provide a substitute for that and must include it with code that makes use of the decorator feature. This leaves some questions though, especially with testing of decorators and so on and where the built-in object is not yet available.
As for the transpiled code, the transpiler needs to generate code that will create instances of the proposed new built-in object and pass these instances to the decorator function instead of just passing target, attr, descriptor. So there is only a slight increase in complexity there.
This is building upon #23 and #61/#65. This can be combined with #66. And it might also be a provable solution for #68. See also #67 where I try to establish a common nomenclature of what types of functions/objects we are dealing with.
Here @lukescott presented a provable solution based on a suggestion made by @jayphelps that would require introduction of another built-in object that will be passed to the decorator function upon decoration.
With the introduction of the
yieldstatement and along with it generators, two new built-in objects were introduced to the language, namelyGeneratorandGeneratorFunction, not to mention the newly introducedSymbol.So what about adding yet another built-in object, say
Additional properties for testing the 'nature' of the descriptor, say
decoratesClass : Boolean, could be added either by default or simply by extending the native object's prototype.And, similar to
Symbol, it should not be instantiated usingnewbut rather by callingDecorationDescriptor(target, attr, descriptor), to "prevent" the casual user from using it.Instances of this would then be passed to the decorator function upon decoration which then could use a simple
instanceofcheck to determine whether it is being used as a decorator or whether it was called with a different set of arguments, for example in cases where we have parameterizable decorators that will return the actual decorator when being called.Having this built-in object, will reduce the signature of the decorator function to just
and will help eliminate the ambiguity that goes along with testing for
typeof arguments[0] == 'function' or typeof arguments[0] == 'object'and so on from a decorator developer's perspective, considering the sort-of proposal found in #23.Existing transpilers, or even native engine code, would then need to create a new instance of proposed decoration descriptor, whatever the built-in object's final name would be, and pass it to the decorator function upon decoration.
All of this, of course, goes hand in hand with the proposal made in both #23 and #61, namely eliminate the need for having to use a call statement when not requiring any additional parameters for an otherwise parameterizable decorator, e.g.
would become (simplified example here!)
With that in place, the decorator could then be used as follows
Class Decorator Use
Desugaring
and so on.
Considering #68, the generated code can now safely assume that the decorator function returned an instance of DecorationDescriptor or did not have any return result.
The latter of course implies that the decorator is able to modify the descriptor in place, given the above interface proposal.
Caveats
Misuse
Of course, this will fail if the user decides to pass in a custom instance of
DecorationDescriptor, e.g.But then again, nothing is safe from being misused. However, this might also be a nice feature to have for frameworks that need to augment existing classes after that they have been declared.
Breaking Change
Existing decorators need to be reimplemented.
Increased Complexity of the Decorator
Decorators, even those that are not parameterizable, need to implement additional guards and also need to be implemented similarly to those that are parameterizable. But I do not see anything wrong in that, e.g.
and its use
would be streamlined with that of parameterizable decorators. And, if the author of the
simpledecoratordecides to make it a parameterizable one, there will be no change in behavior unless these parameters are mandatory. So win-win for everyone, or is it not?Increased Complexity in Transpiler / Engine
The engine needs to implement a new built-in object. Given that engines do not provide for such, the transpiler needs to provide a substitute for that and must include it with code that makes use of the decorator feature. This leaves some questions though, especially with testing of decorators and so on and where the built-in object is not yet available.
As for the transpiled code, the transpiler needs to generate code that will create instances of the proposed new built-in object and pass these instances to the decorator function instead of just passing
target, attr, descriptor. So there is only a slight increase in complexity there.