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
);