DEV Community

Adam Crockett πŸŒ€
Adam Crockett πŸŒ€

Posted on

Simple trick to instance a class without `new`.

I have a case where I want a callable object, or at-least the feeling of:

{}()
{}.prop
Enter fullscreen mode Exit fullscreen mode

I am not the biggest fan of classes in the world but I have to admit it they have been getting some special treatment of late so in my case its unavoidable. private fields, class field and more. Anyway to achieve the following in a non hacky simple kind of way we need to do this:

Worth a note that this is typescript but should work in JavaScript too.

class Dog {
   static legs = 5;
   constructor() {
       console.log('woof');
   }
}

// progmatic use of `new` via .construct
// preload the first argument with the class we want to call;
// proxy the actual Reflect.construct method but point all gets and sets to the static Class constructor, in english: makes static available NOTE this does not mess with Reflect.construct
const callableObject = new Proxy(
  Reflect.construct.bind(null, Dog),
  {
    get(tar, prop, val) {
      // access static 
      return Reflect.get(Dog, prop, val);
    },
    set(tar, prop, val) {
      // access static 
      return Reflect.set(Dog, prop, val);
    },
    apply(target, thisArg, argumentsList) {
      // make the constructor work 
      return target({...argumentsList, length: argumentsList.length});
    }
  }
);
callableObject(); // calls constructor
callableObject.legs; // 5
Enter fullscreen mode Exit fullscreen mode

magic :)

Top comments (5)

Collapse
 
arqex profile image
Javier Marquez

Reflection is always so magic! I've used them in a couple of situations in my code, and when I come back to them, I have trouble to understand what's happening because it's too magical to remember Β―_(ツ)_/Β―

Collapse
 
adam_cyclones profile image
Adam Crockett πŸŒ€

I know thats the problem, they are not idiomatic. I tend to write libraries, lots of libraries and I always want to give the best experience for developers, usually thats why my libraries never get finished. So I do usually avoid magic, the thing is I am doing code generation via typescript and reflection and so I needed to do this stuff.

class codegen {
  @glsl drawCircle(a: float, b: float, c: vec2): float {
    return 0;
  }

  @glsl drawTri(a: float): vec2 {
    return [];
  }
}


GLR({
  el: "#app",
  coorinate: "px",
  frag(gl) {
    gl.fragColor();
  },
  vert(gl) {
    gl.uniform<vec4>("color");

    gl.main(() => {
      gl.drawCircle(1, 2, 3);
      gl.position();
    });
  },
  codegen,
  state: {
    color: rgba(0, 0, 0, 255)
  },
  watch: {
    foo: ({ value }: { value: any }) => {
      console.log("oh hey!", value);
    }
  }
});

The above code is a snippet from my abstraction over Regl, I am attempting to create a reactive webgl framework in the style of Vue.js.

Collapse
 
arqex profile image
Javier Marquez

Reactive webgl, that sounds like a great idea! I hope you share it!

I completely see the point of reflection within libraries, abstracting their "magic" from the developers that use them and offering features that weren't possible before.

I struggled when I use reflection for the business logic... I just won't do it anymore :D

Collapse
 
yawaramin profile image
Yawar Amin • Edited

I highly recommend writing a helper function which returns an instance of the class. This lets you exactly control the return type. For example, that's how I prefer to do private fields/methods/etc.:

// dog.ts

export interface Dog {
  speak(): string
}

// The helper is exported and ensures that only the interface (public) fields are shown
export function newInstance(name: string): Dog {
  return new Impl(name)
}

// The class is hidden
class Impl {
  constructor(name: string) {
    this.name = name
  }

  speak(): string {
    return 'Woof!'
  }

  name: string
}
Collapse
 
adam_cyclones profile image
Adam Crockett πŸŒ€ • Edited

To keep typings do this (@ts-ingore is required but everything still works):


type ConstructedClass = (...args: any[]) => GLRConstructor;
// @ts-ignore
export const callableObject: typeof Dog & ConstructedClass = Reflect.construct.bind(
  null,
  Dog
);
Collapse
 
gepid profile image
Jan Roman Cisowski

My English skills might not be perfect, so I apologise if my ideas overlap with earlier contributions. Nevertheless, I’d like to share my approaches to solving the problem of calling a constructor in TypeScript without using the new keyword.

Nevertheless, as in the topic 'Simple trick to instance a class without new.' , Here are my solutions to the problem, each addressing different scenarios and requirements.:


🟨1οΈβƒ£πŸ…°οΈ

class FactoryBasedClass {
    private constructor(public readonly name: string) {}

    public static create(name: string): FactoryBasedClass {
        return new FactoryBasedClass(name);
    }
}

// Example of use:
const instance1 = FactoryBasedClass.create('Example 1');
console.log(instance1.name); // "Example 1"

// Error: β€˜new’ cannot be used
// const instance2 = new FactoryBasedClass('Error'); // Error: Constructor of class 'FactoryBasedClass' is private.
Enter fullscreen mode Exit fullscreen mode

The utilisation of this particular pattern serves to guarantee that instances of a given class are created exclusively through the designated factories or static methods. This functionality is particularly advantageous in scenarios where stringent control over the initialisation of objects is required.

🟩1οΈβƒ£πŸ…±οΈ

export class MyClass {
    private myVariable1: string;
    private myVariable2: string;

    // Private constructor - prevents the creation of instances using `new`.
    private constructor(myVariable1: string, myVariable2: string) {
        this.myVariable1 = myVariable1;
        this.myVariable2 = myVariable2;
    }

    // Factory method for creating an instance
    public static create(myVariable1: string, myVariable2: string): MyClass {
        return new MyClass(myVariable1, myVariable2);
    }

    // Example of a method that returns the full string
    public getCombined(): string {
        return `${this.myVariable1}/${this.myVariable2}`;
    }
}

// Example of use:
const instance = MyClass.create("https://example.com", "path/to/resource");
console.log(instance.getCombined()); // "https://example.com/path/to/resource"

Enter fullscreen mode Exit fullscreen mode


πŸŸͺ2οΈβƒ£πŸ…°οΈ

class MySingleton {
    // Static variable storing the only instance of a class
    private static instance: MySingleton | null = null;

    // Private constructor - prevents the use of β€˜new’ from outside
    private constructor() {
        console.log('Instancja klasy MySingleton zostaΕ‚a utworzona!');
    }

    // Public static method to access an instance of a class
    public static getInstance(): MySingleton {
        if (this.instance === null) {
            this.instance = new MySingleton();
        }
        return this.instance;
    }

    // Example of instance method
    public sayHello(): void {
        console.log('Hello from MySingleton!');
    }
}

// Example of use:
const singleton1 = MySingleton.getInstance();
singleton1.sayHello();

const singleton2 = MySingleton.getInstance();
console.log(singleton1 === singleton2); // true

Enter fullscreen mode Exit fullscreen mode
  1. The term private constructor refers to a constructor that is not publicly accessible. The constructor is private, preventing the creation of class objects using the new keyword outside the class.
  2. Static getInstance method: This method is employed to create or return an existing instance in a controlled manner.
  3. Single instance (Singleton): By storing instances in the static instance property, it is ensured that only one instance of this class is ever created.

πŸŸ₯2οΈβƒ£πŸ…±οΈ

export class ConfigurableClass {
    private static _instance: ConfigurableClass | null = null;
    private configuration: Record<string, any>;

    private constructor(config: Record<string, any>) {
        this.configuration = config;
    }

    // Setter to create or update an instance
    public static set config(config: Record<string, any>) {
        this._instance = new ConfigurableClass(config);
    }

    // Getter to retrieve an instance
    public static get instance(): ConfigurableClass {
        if (!this._instance) {
            throw new Error("ConfigurableClass has not been configured yet!");
        }
        return this._instance;
    }

    // Example method of accessing to the configuration
    public getConfig(key: string): any {
        return this.configuration[key];
    }
}

// Example of use:
ConfigurableClass.config = { apiEndpoint: "https://api.example.com", timeout: 5000 };
const instance = ConfigurableClass.instance;
console.log(instance.getConfig("apiEndpoint")); // "https://api.example.com"

Enter fullscreen mode Exit fullscreen mode


🟧3οΈβƒ£πŸ…°οΈ

export class AsyncFactoryClass {
    private myVariable1: string;
    private myVariable2: string;

    private constructor(myVariable1: string, myVariable2: string) {
        this.myVariable1 = myVariable1;
        this.myVariable2 = myVariable2;
    }

    // Asynchronous factory method for instance creation
    public static async createAsync(param1: string, param2: string): Promise<AsyncFactoryClass> {
        // You can add additional initialisation logic here
        await new Promise(resolve => setTimeout(resolve, 100)); // Simulation of delay
        return new AsyncFactoryClass(param1, param2);
    }

    // Example of method
    public getCombined(): string {
        return `${this.myVariable1}/${this.myVariable2}`;
    }
}

// Example of use:
(async () => {
    const instance = await AsyncFactoryClass.createAsync("https://example.com", "path/to/resource");
    console.log(instance.getCombined()); // "https://example.com/path/to/resource"
})();

Enter fullscreen mode Exit fullscreen mode

🟫3οΈβƒ£πŸ…±οΈ

export class AsyncSingletonClass {
    private static instance: AsyncSingletonClass | null = null;
    private myVariable: string;

    private constructor(myVariable: string) {
        this.myVariable = myVariable;
    }

    // Asynchronous method for accessing a Singleton instance
    public static async getInstance(param: string): Promise<AsyncSingletonClass> {
        if (!AsyncSingletonClass.instance) {
            await new Promise(resolve => setTimeout(resolve, 100)); // Simulation of delay
            AsyncSingletonClass.instance = new AsyncSingletonClass(param);
        }
        return AsyncSingletonClass.instance;
    }

    // Example of method
    public getValue(): string {
        return this.myVariable;
    }
}

// Example of use:
(async () => {
    const instance = await AsyncSingletonClass.getInstance("Initialized");
    console.log(instance.getValue()); // "Initialized"
})();

Enter fullscreen mode Exit fullscreen mode


Feature Example 🟨1οΈβƒ£πŸ…°οΈ (Factory) Example 🟩1οΈβƒ£πŸ…±οΈ (Static Factory) Example πŸŸͺ2οΈβƒ£πŸ…°οΈ (Singleton) Example πŸŸ₯2οΈβƒ£πŸ…±οΈ (set/get) Example 🟧3οΈβƒ£πŸ…°οΈ (Asynchronous Factory) Example 🟫3οΈβƒ£πŸ…±οΈ (Asynchronous Singleton)
Use Cases Controlled object creation. Simplicity and ease of implementation. When a single instance is required globally. Dynamic configuration changes on the fly. Asynchronous object initialisation. When a single asynchronous instance is required.
Control Over Instance Creation Allows adding creation logic. Enables additional factory logic. Full control over a single instance. Enforces configuration before usage. Supports asynchronous initialisation logic. Provides control over global asynchronous state.
Performance Highly efficient for simple cases. Efficient and easy to use. Minimal overhead after initialisation. Efficient unless the configuration changes often. Dependent on initialisation logic complexity. Requires resources for async/await handling.
Implementation Complexity Medium, requires thoughtful factory logic. Simple, no asynchronous handling needed. Very simple, intuitive implementation. Simple, only requires set and get. Higher, requires async/await handling. Higher, combines async with Singleton logic.
Code Readability Requires familiarity with the factory pattern. Very intuitive and readable. Very simple and clear. Simple, especially for beginners. May be harder for less experienced developers. Clear but requires knowledge of async.