I have class decorator which ensures that the class remains a singleton.
interface Type<T = any> extends Function {
new (...args: any[]): T;
}
function Singleton() {
let instance: Object;
return function <SingletonFunction extends Type>(
constructor: SingletonFunction
) {
return <SingletonFunction>class {
constructor(...args: any[]) {
if (instance) {
return instance;
} else {
instance = new constructor(...args);
Object.setPrototypeOf(instance, constructor);
return instance;
}
}
};
};
}
When I use this decorator on a class like this,
@Singleton()
class TestSingleton {
private field: any;
private field2 = Math.random();
constructor(args: any[]) {
this.field = args.reduce((acc, ele) => {
return acc + ele;
}, 0);
}
}
const x = new TestSingleton([1, 2, 3, 4, 5, 6]);
const y = new TestSingleton([6, 7, 8, 9, 1]);
console.log(x === y); // true
console.log(x instanceof TestSingleton); // false
The instanceof
operator does not work, I can not figure out what I am missing here.
Ignoring the attempt to Object.setPrototypeOf(instance, constructor)
, the problem is with instance = new constructor(...args);
. This calls the old (wrapped, decorated) constructor, which returns an object inheriting from the old .prototype
. However your decorator completely replaces the TestSingleton
class (which it must, to overwrite the constructor), and instanceof TestSingleton
will therefor check inheritance against the new (returned) TestSingleton.prototype
object.
The conventional approach is to just return a class
that inherits from the original (decorated) class, and have the constructor return instances of the subclass (as it creates them by default, if you didn't have your singleton instance
code overriding the constructor return value).
However, this creates an unnecessary extra prototype object and unnecessary extra prototype chain link. I would rather recommend to return a class
(or simple function
) that has exactly the same prototype as the decorated one:
function Singleton<Args extends any[], Res extends object>(
constructor: { new (...args: Args): Res }
) {
let instance: Res;
function newConstructor(...args: Args) {
if (new.target != newConstructor) throw new Error('not supported');
if (instance) {
return instance;
} else {
instance = new constructor(...args);
return instance;
}
};
newConstructor.prototype = constructor.prototype;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
newConstructor.prototype.constructor = newConstructor; // properly hide the decorated original constructor
return newConstructor as unknown as { new (...args: Args): Res };
}
@Singleton
class TestSingleton {
…
}