typescript

Add methods to class dynamically with types?


Is it possible to make def type safe, without manually defining the interfaces?

// With proper `Cat.meow(message: string)` declaration on `Cat`?
const Cat = function() {}
def(Cat, {
  meow: function(message: string) { console.log('meow ' + message) },
})

const cat = new Cat()
cat.meow('I want food')

// Implementation
function def<T>(klass: T, fns: { [k: string]: any }) {
  for (let k in fns) {
    Object.defineProperty(klass.prototype, k, { value: fns[k],
      enumerable: false, writable: true, configurable: true
    })
  }
}

P.S. Maybe with some Babel AST transform magic for TypeScript?


Solution

  • It's conceptually cleaner to just have def() return the desired constructor instead of trying to mutate the type of its klass argument. Still, this is possible by making it an assertion function that narrows the type of the klass argument to one which has an appropriate construct signature.

    Possibly 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 accounts for klass having some argument list of generic type A, and tries to give a reasonably useful experience around use of this inside the klass and fns properties by using a this parameter on klass and the magic ThisType<T> utility type on fns. The idea is so that whatever this is in klass and whatever type fns is will be contextually available as this inside of the argument to fns.

    So the following compiles and works as desired:

    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
    

    Playground link to code