Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assigning properties to non-prototype with decorators

I am building a simple mapping between frontend/backend data structure. In order to do that I've created a decorator that looks like the following:

function ApiField(
    apiKey: string,
    setFn: (any) => any = (ret) => ret,
    getFn: (any) => any = (ret) => ret
) {
    return function (target: AbstractModel, propertyKey: string) {
        target.apiFieldsBag = target.apiFieldsBag || {};
        _.assign(
            target.apiFieldsBag,
            {
                [propertyKey]: {
                    apiKey: apiKey,
                    setFn: setFn,
                    getFn: getFn
                }
            }
        );
    };
}

And this is how I use it:

class AbstractCar {
    @ApiField('id')
    public id: string = undefined;
}

class BMW extends AbstractCar {
    @ApiField('cylinders')
    public cylinderCount: number;
}

class VW extends AbstractCar {
    @ApiField('yearCompanyFounded')
    public yearEstablished: number;
}

The issue that I'm seeing is that instead of the actual object being passed to the decorator it's always its prototype:

__decorate([
    ApiField('yearCompanyFounded')
], VW.prototype, "yearEstablished", void 0);

Which means that as I am assigning stuff to the instance in the decorator, it is always attached to the prototype which in turn means that properties I want to be defined only the VW instance are also available on the AbstractCar and the BMW class (in this example this would be yearEstablished). This makes it impossible to have two properties with the same name but different API fields in two different classes.

Is there any way to circumvent this behaviour?

like image 890
Nik Avatar asked Nov 25 '15 11:11

Nik


2 Answers

Right now, all three classes are adding properties to the same object. The key to fix this is to clone the object on target.data so that each class is using a different object instead of all of them referring to the same object.

Here's a simpler example that demonstrates one way of doing this:

function ApiField(str: string) {
    return function (target: any, propertyKey: string) {
        // I tested with Object.assign, but it should work with _.assign the same way
        target.data = _.assign({}, target.data, {
            [propertyKey]: str
        });
    };
}

class AbstractCar {
    @ApiField("car")
    public carID;
}

class BMW extends AbstractCar {
    @ApiField("bmw")
    public bmwID;
}

class VW extends AbstractCar {
    @ApiField("vw")
    public vwID;
}

AbstractCar.prototype.data; // Object {carID: "car"}
BMW.prototype.data;         // Object {carID: "car", bmwID: "bmw"}
VW.prototype.data;          // Object {carID: "car", vwID: "vw"}
like image 56
David Sherret Avatar answered Nov 03 '22 08:11

David Sherret


The problem is that public inside a class is not standard JavaScript, it’s only something that TypeScript does. Therefore, you have to be careful, because anything you do may break in the future.

One possibility is to use Object.assign() to add instance properties (IINM, apiFieldsBag should be transferred from the object created by the object literal to this):

class AbstractCar {
    constructor() {
        Object.assign(this, {
            @ApiField('id')
            id: undefined,
        });
    }
}
like image 33
Axel Rauschmayer Avatar answered Nov 03 '22 08:11

Axel Rauschmayer