2

I have the following Typescript code:

class Foo {
    private _id: number;
    private _desc: string;

    constructor(id: number, desc: string) {
        this._id = id;
        this._desc = desc;
    }

    public get id(): number {
        return this.id;
    } 

    public set id(value: number) {
        this._id = value;
    }

    public get desc():string {
        return this.desc;
    } 

    public set desc(value: string) {
        this._desc = value;
    }
}

let foo = new Foo(1, 'something');

I would like to get a string from a Typescript class, I mean with getter and setter. Then I should get the following string:

{"id":1,"desc":"something"}

According to this answer I can reach that adding the following method to the class:

public toJSONString(): string {
    return JSON.stringify(this, Object.keys(this.constructor.prototype));
}

It works.

It doesn't work if the Typescript class contains any other sub class.

So if I have the following code:

class Foo {
    private _id: number;
    private _desc: string;
    private _user: Bar;

    constructor(id: number, desc: string, user: Bar) {
        this._id = id;
        this._desc = desc;
        this._user = user;
    }

    public get id(): number {
        return this._id;
    } 

    public set id(value: number) {
        this._id = value;
    }

    public get desc():string {
        return this._desc;
    } 

    public set desc(value: string) {
        this._desc = value;
    }

    public get user(): Bar {
        return this._user;
    } 

    public set user(value: Bar) {
        this._user = value;
    }

    public toJSONString(): string {
        return JSON.stringify(this, Object.keys(this.constructor.prototype));
    }
}

class Bar {
    private _name: string;
    private _surname: string;

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

    public get name(): string {
        return this._name;
    } 

    public set name(value: string) {
        this._name = value;
    }

    public get surname():string {
        return this._surname;
    } 

    public set surname(value: string) {
        this._surname = value;
    }
}

let foo = new Foo(1, 'something', new Bar('foo', 'bar'));

If I use toJSONString method I get the following string:

{"id":1,"desc":"something","user":{}}

instead of this:

{"id":1,"desc":"something","user":{ "name": "foo", "surname": "bar"}}

So, how can I get a string from a Typescript class that has other sub classes?

(If you need here is the playground for the first code and here is the playground for the second code)

smartmouse
  • 13,912
  • 34
  • 100
  • 166

1 Answers1

1

There are 2 things to keep in mind here:

  1. When you define getters and setters they do not become instance methods once transpiled into Javascript, but they are added to the prototype using Object.defineProperty. This means you won't get them simply using JSON.stringify

  2. Passing the replacer array to JSON.stringify and telling it to use only the prototype values does the job but it kinda doesn't work on nested objects. Truth is JSON.stringify will parse only the properties with that name no matter where it resides in the object structure.

For Example

let a = {
    user: "Foo",
    data: {
        name: "Bar"
    }
};

JSON.stringify(a, ["user", "data"]);

Will output {"user":"Foo","data":{}} because even though the nested object's key is data, the object itself does not have properties named user or data
But

let a = {
    user: "Foo",
    data: {
        user: "Bar"
    }
};

JSON.stringify(a, ["user", "data"]);

Will output {"user":"Foo","data":{"user":"Bar"}} because the nested object has a proprety called user, just like its parent

I reckon this behavior can be confusing but it's possible to implement a solution by creating a method that gets all the properties of all the objects you are interested in. I haven't found a way in Typescript to check whether a class implements a interface (or extends a class) so I had to work a bit with what I know it works even though it's not that "elegant".

abstract class Stringifyiable {
    private isStringifyiable(value): boolean {
        return value != null && (typeof value === 'object' || typeof value === 'function') && value['getJsonKeys'] && typeof value['getJsonKeys'] === 'function';
    }

    public getJsonKeys(): string[] {
        let keys = Object.keys(this.constructor.prototype);
        keys.forEach(key => {
            if (this.isStringifyiable(this[key])) {
                keys = keys.concat(this[key].getJsonKeys());
            }
        });

        return keys;
    }

    public toJSONString(): string {
        return JSON.stringify(this, this.getJsonKeys());
    }
}



class Foo extends Stringifyiable {
    private _id: number;
    private _desc: string;
    private _user: Bar;

    constructor(id: number, desc: string, user: Bar) {
        super();
        this._id = id;
        this._desc = desc;
        this._user = user;
    }

    public get id(): number {
        return this._id;
    } 

    public set id(value: number) {
        this._id = value;
    }

    public get desc():string {
        return this._desc;
    } 

    public set desc(value: string) {
        this._desc = value;
    }

    public get user(): Bar {
        return this._user;
    } 

    public set user(value: Bar) {
        this._user = value;
    }
}



class Bar extends Stringifyiable {
    private _name: string;
    private _surname: string;

    constructor(name: string, surname: string) {
        super();
        this._name = name;
        this._surname = surname;
    }

    public get name(): string {
        return this._name;
    } 

    public set name(value: string) {
        this._name = value;
    }

    public get surname():string {
        return this._surname;
    } 

    public set surname(value: string) {
        this._surname = value;
    }
}

let foo = new Foo(1, 'something', new Bar('foo', 'bar'));
//this will output {"id":1,"desc":"something","user":{"name":"foo","surname":"bar"}}
foo.toJSONString();

Be careful with cyclic references because it will go into an endless loop (I'm sure it can be fixed though).

smartmouse
  • 13,912
  • 34
  • 100
  • 166
valepu
  • 3,136
  • 7
  • 36
  • 67