Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript decorators with inheritance

I am playing around with Typescript decorators and they seem to behave quite differently than what I'd expect when used alongside with class inheritance.

Suppose I have the following code:

class A {
    @f()
    propA;
}

class B extends A {
    @f()
    propB;
}

class C extends A {
    @f()
    propC;
}

function f() {
    return (target, key) => {
        if (!target.test) target.test = [];
        target.test.push(key);
    };
}

let b = new B();
let c = new C();

console.log(b['test'], c['test']);

Which outputs:

[ 'propA', 'propB', 'propC' ] [ 'propA', 'propB', 'propC' ]

Though I'd expect this:

[ 'propA', 'propB' ] [ 'propA', 'propC' ]

So, it seems that target.test is shared between A, B and C. And my understanding of what is going on here is as follow:

  1. Since B extends A, new B() triggers the instantiation of A first, which triggers the evaluation of f for A. Since target.test is undefined, it is initialized.
  2. f is then evaluated for B, and since it extends A, A is instantiated first. So, at that time, target.test (target being B) references test defined for A. So, we push propB in it. At this point, things go as expected.
  3. Same as step 2, but for C. This time, when C evaluates the decorator, I would expect it to have a new object for test, different than that defined for B. But the log proves me wrong.

Can anyone explain to me why this is happening (1) and how would I implement f such that A and B have separate test properties ?

I guess you'd call that an "instance specific" decorator ?

like image 956
user5365075 Avatar asked May 11 '17 09:05

user5365075


People also ask

Does TypeScript allow inheritance?

TypeScript supports single inheritance and multilevel inheritance. We can not implement hybrid and multiple inheritances using TypeScript. The inheritance uses class-based inheritance and it can be implemented using extends keywords in typescript.

Are decorators useful in TypeScript?

Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. NOTE Decorators are an experimental feature that may change in future releases.

How does inheritance work in TypeScript?

Inheritance in TypeScriptTypescript uses class-based inheritance which is simply the syntactic sugar of prototypal inheritance. TypeScript supports only single inheritance and multilevel inheritance. In TypeScript, a class inherits another class using extends keyword.

What is target in decorator TypeScript?

target: Constructor function of the class if we used decorator on the static member, or prototype of the class if we used decorator on instance member. In our case it is firstMessage which is an instance member, so the target will refer to the prototype of the Greeter class. propertyKey: It is the name of the property.


2 Answers

Alright, so after spending a few hours playing around and searching the web, I got a working version. I don't understand why this is working, so please forgive the lack of explanation.

The key is to use Object.getOwnPropertyDescriptor(target, 'test') == null instead of !target.test for checking the presence the test property.

If you use:

function f() {
    return (target, key) => {
        if (Object.getOwnPropertyDescriptor(target, 'test') == null) target.test = [];
        target.test.push(key);
    };
}

the console will show:

[ 'propB' ] [ 'propC' ]

Which is almost what I want. Now, the array is specific to each instance. But this means that 'propA' is missing from the array, since it is defined in A. Hence we need to access the parent target and get the property from there. That took me a while to figure out, but you can get it with Object.getPrototypeOf(target).

The final solution is:

function f() {
    return (target, key) => {
        if (Object.getOwnPropertyDescriptor(target, 'test') == null) target.test = [];
        target.test.push(key);

        /*
         * Since target is now specific to, append properties defined in parent.
         */
        let parentTarget = Object.getPrototypeOf(target);
        let parentData = parentTarget.test;
        if (parentData) {
            parentData.forEach(val => {
                if (target.test.find(v => v == val) == null) target.test.push(val);
            });
        }
    };
}

Which outputs

[ 'propB', 'propA' ] [ 'propC', 'propA' ]

May anyone that understands why this works while the above doesn't enlighten me.

like image 91
user5365075 Avatar answered Sep 29 '22 10:09

user5365075


I think it's becouse when class B is created prototype of A is copied with all it's custom properties (as references).

I used slightly modified solution, seams to solve more natural problem with duplicates if class C would not have any decorators.

Still not sure if this is the best way to handle such cases:



    function foo(target, key) {
        let
            ctor = target.constructor;

        if (!Object.getOwnPropertyDescriptor(ctor, "props")) {
            if (ctor.props)
                ctor.props = [...ctor.props];
            else
                ctor.props = [];
        }

        ctor.props.push(key);
    }

    abstract class A {
        @foo
        propA = 0;
    }

    class B extends A {
        @foo
        propB = 0;
    }

    class C extends A {
        @foo
        propC = 0;
    }

like image 32
pankleks Avatar answered Sep 29 '22 10:09

pankleks