How to convert a TypeScript class instance with getter/setter to JSON to store in MySQL database

Viewed 693

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?

1 Answers

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.

Related