I need the JSON.stringify
method to output getters. I understand that it's not doing this because getters are not "enumerable".
I found a solution for that and I am using Object.defineProperty
to greater the getter with an enumerable property as shown in the Playground.
I would like to do the same using a decorator but it doesn't work as shown here.
Please do not suggest implementing the toJSON
method, I know that it may help but I want a decorators-only solution.
It's also relevant for Angular as I need that to make formGroup.patchValue
work for getters.
I updated my example with @acdcjunior suggestion - hope they will find what I am missing here.
JSON.stringify()
, only serializes the object's own enumerable properties. With that decorator you are adding the getters as properties of the prototype object.
From the ES6 Spec:
24.3.2.3 Runtime Semantics: SerializeJSONObject ( value )
The abstract operation SerializeJSONObject with argument value serializes an object. It has access to the stack, indent, gap, and PropertyList values of the current invocation of the stringify method.
(...)
- If PropertyList is not undefined, then
- Let K be PropertyList.
- Else,
- Let K be EnumerableOwnNames(value).
The relevant point is 6
.
If you ask, PropertyList is related to the serialization of arrays, so it couldn't be an escape hatch.
With a method level decorator, although you said you didn't want it, the only way I can think of is to have the decorator add, if not exists, a toJSON
to the prototype of the class.
Such toJSON
would (when first called, only) add the getters from the class prototype (as enumerable properties) to the instance whhich toJSON
is being called upon.
The tricky part would be, when you have multiple getters decorated, to, when processing the second and on decorators, differentiate if the toJSON
you have was either created by you (and then you can "append" a property of the current decorator) or if it was in the class originally (in which case you would ignore). Tricky, but, as far as I can tell, doable.
Per request, there you go. JSFiddle demo here.
class MyClass {
constructor(public name) {}
@enumerable(true)
get location(): string {
return 'Hello from Location!'
}
@enumerable(true)
get age(): number {
return 12345;
}
get sex(): string {
return "f";
}
}
class MyClassWithToJson {
constructor(public name) {}
@enumerable(true)
get nickname(): string {
return 'I shall still be non enumerable'
}
toJSON() { return 'Previously existing toJSON()!' }
}
function enumerable(value: boolean) {
return function (target: any, propertyKey: string) {
if (!value) {
// didnt ask to enumerate, nothing to do because even enumerables at the prototype wont
// appear in the JSON
return;
}
if (target.toJSON && !target.__propsToMakeEnumerable) {
return; // previously existing toJSON, nothing to do!
}
target.__propsToMakeEnumerable = (target.__propsToMakeEnumerable || []).concat([{propertyKey, value}])
target.toJSON = function () {
let self = this; // JSFiddle transpiler apparently is not transpiling arrow functions properly
if (!this.__propsToMakeEnumerableAlreadyProcessed) { // so we just do this once
console.log('processing non-enumerable props...'); // remove later, just for testing
let propsToMakeEnumerable = self.__propsToMakeEnumerable;
(propsToMakeEnumerable || []).forEach(({propertyKey, value}) => {
let descriptor = Object.getOwnPropertyDescriptor(self.__proto__, propertyKey);
descriptor.enumerable = true;
Object.defineProperty(self, propertyKey, descriptor);
});
Object.defineProperty(this, '__propsToMakeEnumerableAlreadyProcessed', {value: true, enumerable: false});
}
return this;
}
};
}
let obj = new MyClass('Bob');
console.log(JSON.stringify( obj ));
console.log(JSON.stringify( obj )); // this second time it shouldn't print "processing..."
console.log(JSON.stringify( new MyClassWithToJson('MyClassWithToJson') ));
Updated TypeScript playground link here.