Recently, I ran into a limitation with JSON.stringify with TypeScript classes that use inheritance. Using get/set in TypeScript with inherited classes results in the serialized output not containing the getter properties.
Imagine a real world example with of a base class that has getters/settings and backing parameters like this:
export class BaseClass extends {
private _property: string;
get property(): string {
return _property;
}
}
This will get compiled down to something similiar to this:
export class BaseClass extends {
private _property: string;
constructor() {
Object.defineProperty(BaseClass .prototype, 'property', {
value: this._property,
writable: false
});
}
I’m only pointing this out because get/set in TypeScript, afaik, work this way although it probably varies based on your target output.
At any rate, the problem arises in that if we now have a class that extends BaseClass..
export class MyClass extends BaseClass { }
And then we want to send that object to an API, or simply serialize it, those get defined properties from the base object will not be included. This is just how JSON.stringify works. It really needs an update to handle typical use cases of prototypical inheritance and newer object types.
Anywho, how does one deal with this? Fortunately, any class in JavaScript can define a toJSON method that defines serialization explicitly. We can use this fact to simply walk the prototypical hierarchy and ensure everything is properly serialized to compensate for JSON.stringify’s short-comings. And since this method will also be inherited through prototypical inheritance, we are able to place it on any base class and everything works fine. For example, I use this method on my “BaseClass” and it is utilized from “MyClass.”
// Note that JSON.stringify doesn't work quite properly
// with private properties and getters/setters and inheritance. By default,
// it will not output getter values, but does output private properties (ugh!)
// This is a little work-around based on solutions in the below links to properly handle things
toJSON() {
const jsonObj = {};
let obj = this;
do {
// Walk the proto chain to set all properties
Object.getOwnPropertyNames(obj)
.filter(key => key[0] !== '_' && key !== 'constructor' && key !== 'toJSON')
.map(key => {
let desc: PropertyDescriptor = Object.getOwnPropertyDescriptor(obj, key);
if (desc) {
// jsonObj[key] = typeof desc.get === 'function' ? desc.get() : this[key];
try { jsonObj[key] = this[key]; }
catch (error) { console.error(`Error calling getter ${key}`, error); }
}
});
}
while (obj = Object.getPrototypeOf(obj));
return jsonObj;
}
This method is pretty simple with a do/while loop that walks the prototype chain. It will use “getOwnPropertyDescriptor” to figure out which properties exist for the class in each iteration. If a property has a property descriptor, we know that it’s a model/class defined property and not a root/object inherit class (JavaScript methods like toString() for example). You can see that any property prefixed with “_” (with assumption that it is a private backing property), “constructor,” and “toJSON” are skipped in the serialization.
This was a kind of interesting problem to stumble upon but it’s simple enough to work around so that one can continue to take advantage of inheritance w/ getters.