19

I'm using TypeScript to define some classes and when I create a property, it generates the equivalent to Class1 in the following plunkr:

http://plnkr.co/edit/NXUo7zjJZaUuyv54TD9i?p=preview

var Class1 = function () {
  this._name = "test1";
}

Object.defineProperty(Class1.prototype, "Name", {
  get: function() { return this._name; },
  set: function(value) { this._name = value; },
  enumerable: true
});

JSON.stringify(new Class1()); // Will be "{"_name":"test1"}"

When serializing, it doesn't output the property I just defined.

instance2 and instance3 behave as I'd expect by serializing the defined property. (see the plunkr output).

My actual question is: Is this normal?

If so, how do I work around it in the most efficient way?

Christian Droulers
  • 853
  • 1
  • 8
  • 21

5 Answers5

23

You can define a toJSON() method on your prototype to customize how instances are serialized.

Class1.prototype.toJSON = function () {
    return {
        Name: this.Name
    };
};
JSON.stringify(new Class1()); // Will be '{"Name":"test1"}'
zzzzBov
  • 174,988
  • 54
  • 320
  • 367
  • 1
    I'm marking this as the answer because I ended up implementing toJSON myself. I already had a map of private members to public properties because I was using it to map some JSON from my API to my TypeScript classes. In the toJson methods, I simply use that map to create a new object with all properties with the inverted names. I'll blog about it soon. – Christian Droulers Apr 17 '15 at 20:41
17

If you like to push it forward, give a try to decorators proposal for TypeScript:

According to this proposal (https://github.com/wycats/javascript-decorators):

A decorator is:

  • an expression
  • that evaluates to a function
  • that takes the target, name, and property descriptor as arguments
  • and optionally returns a property descriptor to install on the target object

It goes like this (high level view):

Client code:

@serializable()
class Person {

    constructor(name: string) {
      this._name = name;
    }

    private _name: string;

    @serialize()
    get name() {
      return this._name;
    }

    @serialize('Language')
    get lang() {
      return 'JavaScript';
    }
}

Infrastructure:

const serialized = new WeakMap();

export function serializable(name?: string) {
    return function (target, propertyKey, descriptor) {
        target.prototype.toJSON = function () {
            const map = serialized.get(target.prototype);
            const props = Object.keys(map);
            return props.reduce((previous, key) => {
                previous[map[key]] = this[key];
                return previous;
            }, {});
        }

    }
}

export function serialize(name?: string) {
    return function (target, propertyKey, descriptor) {
        let map = serialized.get(target);
        if (!map) {
            map = {};
            serialized.set(target, map);
        }

        map[propertyKey] = name || propertyKey;
    }
}

UPDATE: I extracted this sample decorator into a repository at https://github.com/awerlang/es-decorators

André Werlang
  • 5,839
  • 1
  • 35
  • 49
  • Very interesting! But not a viable solution in the short term for this project. Thanks! – Christian Droulers Apr 17 '15 at 20:42
  • yep, you're right, but I find it very promising and hope to use it in production very soon – André Werlang Apr 17 '15 at 20:43
  • I know it's a bit late to ask since TypeScript has been updated, but in my case, the WeakMap is always empty even if `serialize` is called. Any idea ? – Jacks Dec 15 '16 at 12:06
  • 2
    @Jacks perhaps you were retrieving using a class as key, instead of a class' prototype? I updated my answer with a working implementation, so take a look. – André Werlang Dec 18 '16 at 19:07
  • 2
    @AndréWerlang Thank you very much for your update. Yes, I was indeed in the first trying to retrieve values by passing the class and not its prototype. But for the verification, I was trying to print the whole WeakMap which was not possible because keys where not enumerable (see [this](http://stackoverflow.com/questions/41164054/typescript-using-decorators-for-custom-json-serialization)). Thanks for clarifying this post years after :) – Jacks Dec 19 '16 at 07:56
  • Hi, I love this solution, but my Typescript is giving me [an error](https://stackoverflow.com/questions/36446480/typescript-decorator-reports-unable-to-resolve-signature-of-class-decorator-whe) when I try to use `@serialize` property decorator, or `@serializable` class decorator: "Unable to resolve signature of property decorator when used as an expression ". I think I'm using [*TypeScript* decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#metadata) , which seem to be different than the [decorators](https://github.com/wycats/javascript-decorators) you linked – Nate Anderson Feb 24 '18 at 19:55
  • For me , if I 1) clarified my functions' return types include `ClassDecorator` (for `serializable`) and `PropertyDecorator` (for `serialize`), 2) explicitly cast to `` and `` to satisfy the return type; that error message goes away! So I can use the rest of your code as-is, I didn't mean to confuse the variations on the way a decorator function can be written; (i.e. TypeScript class decorators can accept the constructor function!) I prefer your syntax here... thanks again! – Nate Anderson Feb 24 '18 at 20:26
  • How exactly did you do that @TheRedPea? Care to share a gist? – Apidcloud Mar 10 '19 at 06:24
  • 1
    @Apidcloud , something like this (note I removed chunks, this code won't run, but I added arrows to show the use of `ClassDecorator` and `PropertyDecorator` https://gist.github.com/theredpea/c4e6c7923467db57b6afd867a0e1ecc1 – Nate Anderson Mar 11 '19 at 02:51
10

Yes, it is by design.

As defined in ECMA, only own enumerable properties are serialized (stringify() is defined in terms of Object.keys(), among others).

Property accessors are defined on prototypes, both in TypeScript and ES6.

And answering your last question, that is the most eficient way to perform this operation.

Beside that, only a) defining an toJSON() to each object part of the serialization, or b) passing a replacer function/array as 2nd argument to JSON.stringify().

Whitelist properties from prototype:

JSON.stringify(instance, Object.keys(instance.constructor.prototype))
André Werlang
  • 5,839
  • 1
  • 35
  • 49
2

Put something here hopefully can help others. What I have do for fix JSON.stringify not serialize properties defined on prototype

var newObject = $.extend(false, {}, orginalObj);

then I notice newObject have instance properties instead of prototype properties. I'm use typescript and get accessor.

slfan
  • 8,950
  • 115
  • 65
  • 78
BeiBei ZHU
  • 343
  • 2
  • 12
1

As you've discovered, it won't serialize properties defined on prototype.

I know it's not ideal, but another option is to do this:

class Class1 {
    private _name = "test1";

    Name: string; // do this to make the compiler happy

    constructor() {
        Object.defineProperty(this, "Name", {
            get: function() { return this._name; },
            set: function(value) { this._name = value; },
            enumerable: true
        });
    }
}

Defining the property on the instance will serialize the property.

David Sherret
  • 101,669
  • 28
  • 188
  • 178