Search code examples
typescripttypescript-typingstypescript-decoratortypescript-class

TypeScript overriding extended class method that uses decorator gives old method


That gives the old value of the method before being overridden. How I can get the final value there?

const a = {} as any

class Parent {
    @D()
    public doSomething(b: any) {
        console.log('parent')
    }
}

class Child extends Parent{
    public override doSomething() {
        console.log('child')
    }
}

const child = new Child()
child.doSomething()

function D () {
    return function(target: any, property: string, descriptor: PropertyDescriptor) {
        console.log('assign')
        a[property] = target[property]
    }
}

setTimeout(a.doSomething, 1000)   // here it gives "parent" but it should be "child"

Here is the demo

How I can assign the actual value there, the one that calls "child"?


Solution

  • In what follows I will be using JavaScript decorators which are supported starting with in TypeScript 5.0. I won't spend time looking at legacy/experimental TypeScript decorators as represented by the code in the question. Legacy decorators aren't actually deprecated, but they are not going to be developed anymore and eventually they will be harder to use than to avoid.


    There are two general issues with the code in the question that prevent it from working as intended.

    First: the decorator function itself is called once per method declaration, not per method call. So the original code only sets a.doSomethingonce. If you want something to happen every time a decorated method is called, you will need your decorator to wrap the method. So the decorator function is called once, and it uses that opportunity to modify the method it's decorating:

    const a = {} as any   
    function D(originalMethod: any, context: ClassMethodDecoratorContext) {
        const methodName = String(context.name);
        function replacementMethod(this: any, ...args: any[]) {
            a[methodName] = this[methodName].bind(this);
            const result = originalMethod.call(this, ...args);
            return result;
        }
        return replacementMethod;
    }
    
    class Parent {
        @D
        public doSomething() {
            console.log('parent')
        }
    }
    

    Next, there's no reasonable way to modify the behavior of subclass methods that override a decorated superclass method. Overriding methods don't interact with their superclass method merely by existing. So if you want a subclass method call to trigger the decorated behavior, you'll either need to decorate the subclass method itself:

    class Child extends Parent {
        @D // <-- explicitly decorate subclass method
        public override doSomething() {
            console.log('childDecorateSub');
        }
    }
    
    const child = new Child();
    child.doSomething(); // "childDecorateSub" 
    a.doSomething(); // "childDecorateSub" 
    

    Or you'll have to call the decorated method on super

    class Child extends Parent {
        public override doSomething() {
            console.log('childSuperCall');
            super.doSomething(); // <-- explicitly call 
            // decorated superclass method
        }
    }
    
    const child = new Child()
    child.doSomething(); // "childSuperCall", "parent" 
    a.doSomething(); // "childSuperCall", "parent" 
    

    It depends on the behavior you want to see. But one of those should hopefully work for you.


    Notice I said "no reasonable way" for subclass methods to inherit the decorator from superclass methods. It might be technically possible to do some crazy proxying of the superclass's prototype so that when you make a subclass it automatically re-decorates methods, but I wouldn't want to begin implementing something like that, since it's likely to be incredibly fragile.

    Playground link to code