I need to serialize an object to json in angular 2.0.0-rc1 when I found out that Typescript's private isn't private at all, and get set property are not outputted through JSON.stringify.
So I set out to decorate the class:
//method decorator
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
//property decorator
function exclude(target: any, propertyKey: string): any {
return { enumerable: false };
}
class MyClass {
test: string = "test";
@exclude
testExclude: string = "should be excluded";
@enumerable(true)
get enumerated(): string {
return "yes";
}
@enumerable(false)
get nonEnumerated(): string {
return "non enumerable"
}
}
let x = new MyClass();
//1st
console.log(JSON.stringify(x));
//2nd
console.log(JSON.stringify(x, Object.keys(MyClass.prototype)));
//3rd
console.log(JSON.stringify(x, Object.keys(x).concat(Object.keys(MyClass.prototype))));//test 3
on Typescript playground, this gives
{"test":"test"}
{"enumerated":"yes"}
{"test":"test","enumerated":"yes"}
but on my project (angular 2.0.0-rc1), this gives
{"test":"test","testExclude":"should be excluded"}
{"enumerated":"yes"}
{"test":"test","testExclude":"should be excluded","enumerated":"yes"}
What I'm really after is output #3 from the playground.
After taking a look at the transpiled code, the only difference is reflect-metadata's code:
//snip ...
__decorate([
exclude,
__metadata('design:type', String)
], MyClass.prototype, "testExclude", void 0);
__decorate([
enumerable(true),
__metadata('design:type', String)
], MyClass.prototype, "enumerated", null);
__decorate([
enumerable(false),
__metadata('design:type', String)
], MyClass.prototype, "nonEnumerated", null);
return MyClass;
}());
none of that __metadata
lines in playground.
What's happening in here? And how can I achieve playground's #3 result on my project?
Fixed it (or might be just a workaround).
Notice that in playground, Reflect-metadata is not available. Property decorators can return an object to be assigned (ORed) to the descriptor to change its behaviour. In angular environment, Reflect-metadata (specifically Reflect.decorate()
) is used instead to decorate things.
After reading up on reflect-metadata doc, and this, apparently there's no way to change PropertyDescriptor on property decorator since it is tied to the constructor instead of the prototype. A solution(workaround) would be to recreate the property with the new descriptor.
function include(value: boolean) {
return function (target: any, propertyKey: string): any {
// Buffer the value
var _val = target[propertyKey];
// Delete property.
if (delete target[propertyKey]) {
// Create new property with getter and setter
Object.defineProperty(target, propertyKey, {
get: () => _val,
set: (newVal) => _val = newVal,
enumerable: value,
configurable: true
});
}
}
}
the factory is only needed so I could use @include(false)
instead of @exclude
.
Only drawback is the fact that the property now tied to the prototype, hence normal JSON.stringify(instance)
would not serialize it.
On that note, we can further make a generic decorator usable both in property and method, as such:
//method decorator
function excludeMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = false;
return descriptor;
};
//property decorator
function excludeProperty(target: any, propertyKey: string): any {
// Buffer the value
var _val = target[propertyKey];
// Delete property.
if (delete target[propertyKey]) {
// Create new property with getter and setter
Object.defineProperty(target, propertyKey, {
get: () => _val,
set: (newVal) => _val = newVal,
enumerable: false,
configurable: true
});
}
}
function exclude(...args : any[]) {
switch(args.length) {
case 2:
return excludeProperty.apply(this, args);
case 3:
if (typeof args[2] !== "number")
return excludeMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}
so now we can use it as such:
class MyClass {
test: string = "test";
@exclude
testExclude: string = "should be excluded";
get enumerated(): string {
return "yes";
}
@exclude
get nonEnumerated(): string {
return "non enumerable"
}
constructor() {}
}
let x = new MyClass();
//to serialize, we have to whitelist the instance and its prototype prop keys
console.log(JSON.stringify(x, Object.keys(x).concat(Object.keys(MyClass.prototype))));
So far I haven't found a cleaner way to do this.