Instead of mutating the type of its klass
argument, a cleaner concept would be to have def()
return the desired constructor. However, it is still possible to achieve this by creating an assertion function that narrows the type of the klass
argument to one with an appropriate construct signature.
One approach could look something like this:
function def<A extends any[], T extends object, M extends Record<keyof M, Function>>(
klass: (this: T, ...args: A) => void, fns: M & ThisType<T & M>
): asserts klass is typeof klass & (new (...args: A) => T & M) {
for (let k in fns) {
Object.defineProperty(klass.prototype, k, {
value: fns[k],
enumerable: false, writable: true, configurable: true
})
}
}
This allows for klass
to have an argument list of some generic type A
, and aims to provide a useful experience with this
inside both the klass
and fns
properties by leveraging a this
parameter on klass
and the magic ThisType<T>
utility type on fns
. The goal is to make whatever this
refers to in
klass</code and whatever type <code>fns
is accessible as
this
within the argument to
fns
.
In practice, code following this pattern compiles and functions as intended:
const Cat = function () { }
def(Cat, {
meow(message: string) { console.log('meow ' + message) },
})
const cat = new Cat()
cat.meow('I want food') // meow I want food
const Dog = function (this: { name: string }, name: string) {
this.name = name;
}
def(Dog, {
bark() { console.log(this.name + " barks") }
})
const dog = new Dog("Fido");
console.log(dog.name); // Fido
dog.bark(); // Fido barks
Link to code on TypeScript Playground