Automatically pass caller’s scope property as argument in class constructor or method

I have around 50 different classes that extend BaseClass. Some of them initialise internally some others. All instances have a unique id property

The goal is to know which instance initiated which (with some reference to the Caller.id) and the code for this to be in the context of BaseClass.

All initialisations follow this format, they call ‘method’ right after init: new ChildClass().method()

new ChildClass().method().bind(this) is not a good option in my case unfortunately

The code looks like this

class ChildClass extends BaseClass {
 id = 'ChildId';
}

class ParentClass extends BaseClass{
 id = 'ParentId';
 parentMethod() {
  new ChildClass().method()
 }
}

abstract class BaseClass {
 protected id;
 private callerId;

 constructor() {
 // either here we set callerId from params
 }
 method() {
  // or here we set callerId from params
 }
}

I want to avoid doing the following in my ParentClass.

const callerId = this.id;
new ChildClass(callerId).method()

or

const callerId = this.id;
new ChildClass().method(callerId)

But i still need my BaseClass to grab the callerId

Not sure if this is even possible 🙂

Answer

If I understood you correctly, you need to keep a reference/link to the class that instantiated this class. There may be different solutions. The simplest one I foresee is to replace new within your class context:

type Constructor<T> = { new (...args): T, prototype: T };

abstract class BaseClass {
  protected id;
  private callerId;

  _new<T extends BaseClass>(clazz: Constructor<T>, ...args): T {
    const i = new clazz(...args);
    i.callerId = this.id;
    return i;
  }

  method() {
  }
}

class ChildClass extends BaseClass {
  id = 'ChildId';
}

class ParentClass extends BaseClass{
  id = 'ParentId';
  parentMethod() {
    this._new(ChildClass).method();
  }
}

I’m not fully aware of all the voodoo that can be done with the latest JS/TS versions. From my knowledge, there is nothing you can do if you want to keep your code in the form of:

new ChildClass().method();

Why? Well, the swiss knife to add magic to your JS code is Monkey patching. But by design when you use new a new Object is created and passed to the constructor function as the current this context. This behavior makes us lose track of the original reference but can not be altered, hence no solution.

I was lying

In JS you can bend the rules to your will (this is why many developers hate it. Not me of course). One very unusual solution that comes to my mind is inspired by Vue.js value binding core and it would work as follow:

let __BASE_CLASS_ACT_ID = undefined; // (1)

abstract class BaseClass {
  protected id;
  private callerId;

  constructor() {
    // Catch my caller according to the current context
    this.callerId = __BASE_CLASS_ACT_ID;
    // Patch all my methods
    for (const name of Object.getOwnPropertyNames(this)) {
      if (typeof this[name] === 'function') {
        const _old = this[name]; // (3)
        this[name] = (...args) => { // (2)
          __BASE_CLASS_ACT_ID = this.id;
          _old.apply(this, args);
          __BASE_CLASS_ACT_ID = undefined;
        }
      }
    }
  }
}

Here we leverage the fact that JS is a single thread language, hence any function will be called at a time. We keep a global variable (1) that is updated within a patching function (2) that sets it before calling the original function (3) and resets it after. This should allow you to have this working:

parentMethod() {
  new ChildClass().method();
}

Note that this will not working in a constructor:

constructor() {
  super();
  new ChildClass().method(); // not working
}

Nor it will work in an async function:

parentMethod() {
  doSome().then(x => {
    new ChildClass().method(); // not working
  })
}

This example can be improved and strengthened, but it will always remain a hack, where using a simple _new solves your issues straight away.