3

I have the following TypeScript class that contains a getter / setter:

export class KitSection {

    uid: string;
    order: number;

    set layout(layout: KitLayout) {
        this._layout = new KitLayout(layout);
    }

    get layout() {
        return this._layout;
    }

    private _layout: KitLayout;

    constructor(init?: Partial<KitSection>) {
        Object.assign(this, init);
    }

}

// I can create an instance like so:
const section = new KitSection(data);

I need to POST this instance to my server as a JSON object to store in a MySQL database column of type: JSON, so I figured I could do the following:

const jsonSection = JSON.parse(JSON.stringify(section))

This creates a JSON object, but when I inspect in the console, i see my private getter/setter variable instead of the public variable in the object:

console.log(jsonSection);

///IN CONSOLE///

uid: "a"
order: 0
_layout: {_rows: Array(2), columns: 12}

I don't want to store the private variable _layout in my database, I need to store the public counterpart defined in the getter/setter: layout.

Next, I checked out the answer provided here: https://stackoverflow.com/a/44237286/6480913 where it suggests to add the following method to convert to JSON:

public toJSON(): string {
    let obj = Object.assign(this);
    let keys = Object.keys(this.constructor.prototype);
    obj.toJSON = undefined;
    return JSON.stringify(obj, keys);
}

However, this returns an empty object. Upon inspection, when I console.log() the this.constructor.prototype, I see the all the properties of the object, but each object is kind of greyed out, so when we use Object.keys(), I receive an empty array. Why are these constructor properties greyed out?

Jordan Lewallen
  • 1,681
  • 19
  • 54
  • I've discovered that the greyed out (aka dimmed) properties mean that they are not enumerable, which is why they don't show up in Object.keys, but why is that? The default is that these properties ARE enumerable, I didn't set them otherwise – Jordan Lewallen Apr 12 '20 at 04:06

1 Answers1

1

JSON.stringify will only iterate over own enumerable properties. Here, since layout is a property on the prototype object, not the instance itself, when you try to stringify the instance, the getter is not invoked. (but since the _layout is an own enumerable property, it is included in the result)

Similarly, the following stringified object is empty:

const obj = Object.create({
  get prop() {
    return 'val';
  }
});
console.log(JSON.stringify(obj));

You could fix it by putting the getter directly on the instance, and by making the _layout non-enumerable. This way, when stringified, the getter will be invoked, but _layout will not be included:

export class KitSection {

    uid: string;
    order: number;

    private _layout: KitLayout;

    constructor(init?: Partial<KitSection>) {
        Object.defineProperty(
            this,
            'layout',
            {
                enumerable: true,
                get() {
                    return this._layout;
                },
                set(newVal) {
                    this._layout = new KitLayout(newVal);
                }
            }
        );
        Object.defineProperty(
            this,
            '_layout',
            {
                enumerable: false,
                value: undefined,
            }
        );
        Object.assign(this, init);
    }
}

const section = new KitSection(data);

It'll look a bit clearer if you use private class field syntax instead:

export class KitSection {
    #layout: KitLayout | undefined;
    constructor(init?: Partial<KitSection>) {
        Object.defineProperty(
            this,
            'layout',
            {
                enumerable: true,
                get() {
                    return this.#layout;
                },
                set: (newVal) => {
                    this.#layout = new KitLayout(newVal);
                }
            }
        );
        Object.assign(this, init);
    }
}

You could also invoke the getter manually.

If the KitLayout is essentially serializable, to turn the serialized object back into a KitSection instance, KitLayout or one of its helper methods needs to be able to turn a serialized KitLayout into an instance. Either pass it through the constructor again (just call new KitSection, which hands it to KitLayout), or separate the layout from the deserialized object and have a separate method on KitSection that calls KitLayout's helper method and assigns the private property, maybe something like:

integrateLayout(layoutInfo) {
  this.#layout = KitLayout.makeKitLayoutFromLayoutInfo(layoutInfo)
}

where layoutInfo is the plain object.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • thanks so far, a little confused though because even the `uid` and the `order` properties don't show up in `Object.keys` when I run the `toJSON` method, why is that? – Jordan Lewallen Apr 12 '20 at 04:12
  • They don't look to be assigned to the instance anywhere - they only exist in the Typescript annotations, as far as I can see – CertainPerformance Apr 12 '20 at 04:13
  • I suppose I have a fundamental misunderstanding of instances and prototypes (coming from C#), let me read up on some documentation and get back to this – Jordan Lewallen Apr 12 '20 at 04:22
  • alright took me this long to wrap my head around everything! I do believe there is a syntax ordering error in your last block of code, I think the `Object.assign()` needs to be run after the `defineProperty` in order to set the layout instance variable. – Jordan Lewallen Apr 12 '20 at 07:43
  • Are you intending for the `init` object to invoke the setter? If it just contains the `uid` and `order` properties, it shouldn't matter – CertainPerformance Apr 12 '20 at 08:00
  • yes, the object passed through is a partial of the KitSection which will contain a `layout` property – Jordan Lewallen Apr 12 '20 at 08:22