How to call a static method every time a static class method is called in TypeScript?

Viewed 675

I created a singleton (abstract) class that requires to have some data initialized before most methods can be used. This is an example of how the class looks like:

abstract class MyClass {
  private static initialize(): void {
    // do stuff
  }

  public static doStuff1(param1: string): string {
    MyClass.initialize();
    // do stuff
    return 'stuff1';
  }

  public static doStuff2(param2: string[]): string[] {
    MyClass.initialize();
    // do stuff
    return ['stuff2'];
  }
}

Is there a way to avoid adding these initialize() call on each methods? I was thinking of using decorators and found this related question: Typescript | Call a function every time a function is called

Unfortunately, it is using the legacy decorator format, at least this is the error I am getting:

Syntax error - Support for the experimental syntax 'decorators-legacy' isn't currently enabled

I was trying to find an equivalent format using the stage 2 proposal, but maybe there is a better option?

3 Answers

(Singletons are generally discouraged, but as I don't know your use case I will assume you have good reasons to use them and won't go into alternatives).

In order to solve your problem, you could decorate your functions using a little wrapper utility:

const wrap = <T extends (...args: any[]) => any>(func: T): T =>
  <T>((...args: any[]) => {
    // Do something before every function here...
    return func(...args);
  });

You could then define your functions like this:

abstract class MyClass {
  public static doStuff1 = wrap((param1: string): string => {
    // do stuff
    return 'stuff1';
  });
}

Function doStuff1 is now guaranteed to always execute the code in wrap before executing its own.

The problem with this approach is that you can't access the private state or functions of MyClass, which is likely desirable. You could pass a reference to the method every time you wrap it, but then you're not much better off than just calling it at the beginning of every function.

Better approach: non static singleton

If you are using a singleton anyhow, you might be better off implementing it in a non static fashion and have the benefits of guaranteed one time initialization for free:

class MyClass {
  private static instance?: MyClass;
  static getInstance = (): MyClass =>
    (MyClass.instance = MyClass.instance ?? new MyClass());

  private constructor() {
    // Do your initialization
    console.log("initializing");
  }

  doStuff1() {
    // do stuff
    console.log("doing stuff 1");
  }

  doStuff2() {
    // do stuff
    console.log("doing stuff 2");
  }
}

MyClass.getInstance().doStuff1();

// Output:
// initializing
// doing stuff 1

MyClass.getInstance().doStuff1();
MyClass.getInstance().doStuff2();

// Output:
// doing stuff 1
// doing stuff 2

Please note from the above output that initialization is called only once, which (judging by your question) seems to be the desired behavior.

I agree with soimons answer because it encourages a cleaner code. Nevertheless, you might use a proxy object to wrap your abstract class so that your calls feel like you were using a static singleton.

I am not sure if this has side effects, but it seems that it does what you need.

_MyClass.ts

abstract class _MyClass {
  private static count:number = 0;

  private static initialize(): void {
    console.log('initialized'+_MyClass.count++);
  }
  private static doStuff1(param1: string): string {
    return 'stuff1';
  }
  private static doStuff2(param2: string[]): string[] {
    return ['stuff2'];
  }
  public static handler2 = {
    get: function(target:any, prop:Function) {
      _MyClass.initialize();
      return Reflect.get(target,prop);
    }
  };
}
const MyClass = new Proxy(_MyClass, _MyClass.handler2);
export {MyClass};

index.ts

import { MyClass } from "./_MyClass"
alert(MyClass.doStuff1())

the above code will produce the following:

[LOG]: "initialized0" 
[LOG]: "stuff1" 
[LOG]: "initialized1" 
[LOG]: ["stuff2"] 

If this helps anyone, I ended up going a much simpler route, basically exporting an instance of a non-static class. So it's no longer a true singleton which means nothing is preventing the initialization of more instances, but there is really no point (other than for tests) to do so.

class MyClass {
  private prop1: string[];
  private prop2: string;

  constructor(): {
    // load data into the object
    this.prop1 = ['a', 'b'];
    this.prop2 = 'c';
  }

  public getProp1(param: string[]): string[] {
    return this.prop1;
  }

  public getProp2(param: string): string {
    return this.prop2;
  }

}

const myClass = new MyClass();
export default myClass;
Related