How to access class metadata from method decorator
Asked Answered
P

1

9

I'm having two decorators. A class decorator and a method decorator. The class decorator defines metadata which I want to access in the method decorator.

ClassDecorator:

function ClassDecorator(topic?: string): ClassDecorator {
    return (target) => {
        Reflect.defineMetadata('topic', topic, target);
        // I've also tried target.prototype instead of target
        return target;
    };
}

MethodDecorator:

interface methodDecoratorOptions {
    cmd: string
}

function MethodDecorator(options: decoratorOptions) {
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        // HERE IS MY PROBLEM
        console.log('metaData is: ', Reflect.getMetadata('topic', target));
    }
}

And this is my Class definition:

@ClassDecorator('auth')
export class LoginClass {

    @MethodDecorator({
        cmd: 'login'
    })
    myMethod() {
        console.log('METHOD CALLED');
    }
}

THE PROBLEM:

The following line of the MethodDecorator returns metaData is: undefined. Why is it undefined?

console.log('metaData is: ', Reflect.getMetadata('topic', target));

THE QUESTION:

How can I access the metadata defined by the ClassDecorator from the MethodDecorator?

Paraglider answered 19/1, 2018 at 13:53 Comment(0)
S
14

The problem is the order in which decorators get executed. Method decorators are executed first, class decorators are executed after. This makes sense if you think about it, the class decorators need the complete class to act upon, and creating the class involves creating the methods and calling their decorators first.

A simple workaround would be for the method decorator to register a callback that would then be called by the class decorator after the topic was set:

function ClassDecorator(topic?: string): ClassDecorator {
    return (target) => {
        Reflect.defineMetadata('topic', topic, target.prototype);
        let topicFns: Array<() => void> = Reflect.getMetadata("topicCallbacks", target.prototype);
        if (topicFns) {
            topicFns.forEach(fn => fn());
        }
        return target;
    };
}

interface methodDecoratorOptions {
    cmd: string
}

function MethodDecorator(options: methodDecoratorOptions) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        let topicFns: Array<() => void> = Reflect.getMetadata("topicCallbacks", target);
        if (!topicFns) {
            Reflect.defineMetadata("topicCallbacks", topicFns = [], target);
        }
        topicFns.push(() => {
            console.log('metaData is: ', Reflect.getMetadata('topic', target));
        });
    }
}

@ClassDecorator('auth')
class LoginClass {

    @MethodDecorator({
        cmd: 'login'
    })
    myMethod() {
        console.log('METHOD CALLED');
    }
}
Schoonover answered 19/1, 2018 at 14:32 Comment(5)
But to be honest, I don't like that workaround. It seems less readable to me. I hope to find a better solution.Paraglider
I don't know another work around, it does make the code harder to read.. but then again decorators are part of your infrastructure, which you will write once and use many times, and the usage of the decorators does not change which is the more important part in my opinionSchoonover
You can unmark the answer and wait for a better workaround, I don't mind :). But I have seen and used similar workarounds for this problem and have not found a better one yet, only variations on this theme (use a static class field for example instead of the Reflect` api )Schoonover
I didn't mean to offend you. I am very thankful that I do understand the problem now. I don't get why the developers have chosen that order of decorator execution. Maybe they have a reason for that, but for my case it's inconvenient.Paraglider
@Paraglider No offense taken, really :). I am sure there is a standard somewhere that mandates this. And I am sure they considered the other way but the order makes more sense like this for most use cases, unfortunately not for yoursSchoonover

© 2022 - 2024 — McMap. All rights reserved.