Instancing class with an without new with a class decorator

Viewed 39

Looking for ways to instantiate classes without new in typescript, this fine answer https://stackoverflow.com/a/44061744/430531 works nicely in javascript.

The code in the repo referred https://github.com/digital-flowers/classy-decorator is a simple class decorator:

var classy = function classy() {
    return function (Class) {
        var _Class = function _Class() {
            for (var _len = arguments.length, rest = Array(_len), _key = 0; _key < _len; _key++) {
                rest[_key] = arguments[_key];
            }

            return new(Function.prototype.bind.apply(Class, [null].concat(rest)))();
        };
        _Class.prototype = Class.prototype;
        return _Class;
    };
};

But using it under Typescript like this:

@classy()
class Someone{
    constructor(private name: string){}
}
debugger
let some1 = new Someone("john");
let some2 = Someone("joe")  // some2 is also an instance of Someone! but typescript complains that it's not callable

I have yet to dig into the function decorator itself to understand it well and that also has ts errors but is operational.

But I wonder if there's a way to avoid the Typescript error when instancing without new (let some2 = Someone("joe"))

1 Answers

Unfortunately, decorators don't mutate the types of the things they decorate in TypeScript. That means the Someone constructor will not be seen by the compiler to have a call signature. There is a longstanding feature request at microsoft/TypeScript#4881 to support class decorator mutation at the type level, but it's been stuck in limbo for a long time waiting for the proposal for adding decorators to JavaScript to be approved. It's now at Stage 3 of the TC39 process, and microsoft/TypeScript#48885 is tracking implementing JS conformant decorators in TypeScript, but I don't know when or if type mutation will be implemented. For now, this is just not possible.


The main workaround here is to calling the decorator as a regular function, since function outputs are able to be different in type from their inputs. For example, we could make a tsClassy function which we assert to behave as something that takes a constructor as input and returns a callable constructor as output:

const tsClassy = classy() as
  <T, A extends any[], R>(ctor: T & (new (...args: A) => R)) => T & { (...args: A): R };

In order to use it, we can't just decorate Someone. Instead we must call it on a class constructor and save the output to its own variable. Perhaps like this:

const Someone = tsClassy(class Someone {
  constructor(public name: string) { } 
});
//const Someone: typeof Someone & ((name: string) => Someone)

Now the compiler sees Someone both as a class constructor and as a callable function:

let some1 = new Someone("john");
// let some1: Someone
console.log(some1.name) // john 

let some2 = Someone("joe");
//let some2: Someone
console.log(some2.name) // joe

Playground link to code

Related