# Constructor decorators

**URL:** https://es.discourse.group/t/constructor-decorators/1903
**Category:** 💡 Ideas
**Created:** [January 22, 2024, 3:14pm UTC](https://es.discourse.group/t/constructor-decorators/1903 "2024-01-22T15:14:01Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![Jamesernator](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/jamesernator/32/538_2.png) [@Jamesernator](https://es.discourse.group/u/Jamesernator)
#### Post date: [January 22, 2024, 3:14pm UTC](https://es.discourse.group/t/constructor-decorators/1903/1 "2024-01-22T15:14:01Z")

</div>

Decorators is stage 3 so probably won't get any changes, as such I propose this idea here.

So currently using class decorators to override `constructor` has the footgun that you break encapsulation of the class, for example consider the following decorator that freezes instances:

```js
function traced(klass, ctx) {
     return class extends klass {
          constructor(...args) {
               console.log("Called constructor", { ctx, args });
               super(...args);
          }
     }
}

@traced
class Point {
     x;
     y;
     constructor(x, y) {
         this.x = x;
         this.y = y;
     }
}

```

Using this pattern still leaks the original class, as it can be trivially obtained just by getting the prototype of the decorated class:

```js
const OriginalPoint = Object.getPrototypeOf(Point);

// Works, this breaks decorators that care about encapsulation
// e.g. if this was recorded for security reasons, then an XSS attacker could
// use this technique to avoid detection
const point = new OriginalPoint(0, 0);

```

This isn't actually repairable as far as I can tell either, because classes use the _current prototype_ to determine `super`, i.e. if we do:

```js
function traced(klass, ctx) {
     const newClass = class extends klass {
          constructor(...args) {
               console.log("Called constructor", { ctx, args });
               super(...args);
          }
     };
     Object.setPrototypeOf(newClass, Object.getPrototypeOf(klass));
     Object.setPrototypeOf(newClass.prototype, Object.getPrototypeOf(klass.prototype));
     return newClass;
}

```

If you do this and try to construct `Point` now you get the error:

```js
Super constructor null of newClass is not a constructor 

```

In order to fully repair the class, I believe the only solution is to use `Reflect.construct` and return override:

```js
function traced(klass, ctx) {
     const newClass = class extends klass {
          constructor(...args) {
               console.log("Called constructor", { ctx, args });
               return Reflect.construct(klass, args, newClass);
          }
     };
     Object.setPrototypeOf(newClass, Object.getPrototypeOf(klass));
     Object.setPrototypeOf(newClass.prototype, Object.getPrototypeOf(klass.prototype));
     return newClass;
}

```

This is quite a bit of boilerplate and nuanced just to maintain encapsulation, especially compared to something like a method decorator which can just return a new function.

## The proposal

I'd like to propose the ability to decorate the constructor function itself directly as if it were a method. i.e. We could write the following and it would work regardless of whether used on a method or constructor:

```js
function traced(methodLike) {
     return function(...args) {
          console.log(`Called ${ methodLike.name }`, args);
          return methodLike.call(this, ...args);
     }
}

class Point {
     x;
     y;

     @traced
     constructor(x, y) {
         // ...
     }

     @traced
     method() {
         
     }
}

```

As for semantics, what I propose is that a constructor decorator is given a function which is basically:

```js
function constructorMethod(...args) {
     return Reflect.construct(
         // This is the original class constructor
         originalClassConstructor,
         args,
         // The final decoratedClass (including class decorators) 
         fullyDecoratedClass,
     );
}

```

The class constructor would simply be replaced by a function which calls the decorated class constructor, i.e. as a function `Point` would be:

```js
function Point(...args) {
    return decoratedConstructorFunction(...args);
}

```

Class decorators would then be applied to this function, with `.prototype`, `[[Prototype]]` and all the usual class features put onto this decorated constructor function.

### FAQ

#### Why do constructor decorators receive a function rather than a constructor?

This is because, even if we were to switch on the decorator type and do `new originalConstructor` the `new.target` would be wrong as we don't have access to the fully decorated class at the stage these decorators are applied.

i.e.:

```js
function traced(methodLike, { kind, name }) {
     if (kind === "method") {
         return function(...args) {
            console.log(`Called method ${ name }`, args);
            return methodLike.call(this, ...args);
         }
     } else if (kind === "constructor") {
          return function(...args) {
              console.log("Called constructor", args);
              // OOPS, wrong new.target again
              return new methodLike(...args);
          }
     }
}

```

---

<div class="post-metadata">

### Author: ![ReinsBrain](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/reinsbrain/32/1165_2.png) [@ReinsBrain](https://es.discourse.group/u/ReinsBrain)
#### Post date: [January 23, 2024, 2:43pm UTC](https://es.discourse.group/t/constructor-decorators/1903/2 "2024-01-23T14:43:34Z")

</div>

Hi @Jamesernator - another good place to get your suggestion noticed is at: [Issues · tc39/proposal-decorators · GitHub](https://github.com/tc39/proposal-decorators/issues)

---

<div class="post-metadata">

### Author: ![aclaymore](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/aclaymore/32/501_2.png) [@aclaymore](https://es.discourse.group/u/aclaymore)
#### Post date: [January 23, 2024, 6:52pm UTC](https://es.discourse.group/t/constructor-decorators/1903/3 "2024-01-23T18:52:44Z")

</div>

Cross-referencing @Jamesernator 's similar issue there [Allow decorating the "constructor" as a function · Issue #419 · tc39/proposal-decorators · GitHub](https://github.com/tc39/proposal-decorators/issues/419)

---

<div class="post-metadata">

### Author: ![aclaymore](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/aclaymore/32/501_2.png) [@aclaymore](https://es.discourse.group/u/aclaymore)
#### Post date: [January 23, 2024, 6:59pm UTC](https://es.discourse.group/t/constructor-decorators/1903/4 "2024-01-23T18:59:03Z")

</div>

> [@Jamesernator](#):
>
> ```javascript
> function traced(methodLike) {
> return function(...args) {
> console.log(`Called ${ methodLike.name }`, args);
> return methodLike.call(this, ...args);
> }
> }
> 
> ```

This pattern looks like it wouldn't work for constructors of child classes, their constructors don't have access `this` before the call to `super()` has completed. So the function returned by the decorator also couldn't be provided with access to `this`.

I think these things are what makes the construction harder to treat the same as a method, their have subtly different semantics around `this` and the interpretation of the return value. Maybe not insurmountable though.

---

<div class="post-metadata">

### Author: ![Jamesernator](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/jamesernator/32/538_2.png) [@Jamesernator](https://es.discourse.group/u/Jamesernator)
#### Post date: [January 24, 2024, 12:35am UTC](https://es.discourse.group/t/constructor-decorators/1903/5 "2024-01-24T00:35:58Z")

</div>

> [@aclaymore](#):
>
> This pattern looks like it wouldn't work for constructors of child classes, their constructors don't have access `this` before the call to `super()` has completed.

Yes they wouldn't be given `this`, although actually thinking about it `this` would be need to be `new.target` so subclassing works, i.e. the wrapper function passed to the decorator would look like:

```js
function decoratedConstructor(...args) {
    // Using this, rather than decoratedClass as decoratedClass itself might
    // be subclassed
    return Reflect.construct(originalConstructor, args, this);
}

```

> [@aclaymore](#):
>
> So the function returned by the decorator also couldn't be provided with access to `this`.
> 
> I think these things are what makes the construction harder to treat the same as a method, their have subtly different semantics around `this` and the interpretation of the return value. Maybe not insurmountable though.

Yes, they are different from methods, however the (to be proposed neet meeting) [function decorators](https://github.com/rbuckton/proposal-function-decorators) would be different from methods too.

The main goal here is to be able to write decorators like `traced`, `cached`, `checkParams`, `checkReturn`, etc, etc that can be applied to _any_ function-like in an easy way.
