How do I initialize a TypeScript object with a JSON object

前端 未结 16 712
被撕碎了的回忆
被撕碎了的回忆 2020-11-22 08:30

I receive a JSON object from an AJAX call to a REST server. This object has property names that match my TypeScript class (this is a follow-on to this question).

Wha

相关标签:
16条回答
  • 2020-11-22 08:51

    JQuery .extend does this for you:

    var mytsobject = new mytsobject();
    
    var newObj = {a:1,b:2};
    
    $.extend(mytsobject, newObj); //mytsobject will now contain a & b
    
    0 讨论(0)
  • 2020-11-22 08:52

    I personally prefer option #3 of @Ingo Bürk. And I improved his codes to support an array of complex data and Array of primitive data.

    interface IDeserializable {
      getTypes(): Object;
    }
    
    class Utility {
      static deserializeJson<T>(jsonObj: object, classType: any): T {
        let instanceObj = new classType();
        let types: IDeserializable;
        if (instanceObj && instanceObj.getTypes) {
          types = instanceObj.getTypes();
        }
    
        for (var prop in jsonObj) {
          if (!(prop in instanceObj)) {
            continue;
          }
    
          let jsonProp = jsonObj[prop];
          if (this.isObject(jsonProp)) {
            instanceObj[prop] =
              types && types[prop]
                ? this.deserializeJson(jsonProp, types[prop])
                : jsonProp;
          } else if (this.isArray(jsonProp)) {
            instanceObj[prop] = [];
            for (let index = 0; index < jsonProp.length; index++) {
              const elem = jsonProp[index];
              if (this.isObject(elem) && types && types[prop]) {
                instanceObj[prop].push(this.deserializeJson(elem, types[prop]));
              } else {
                instanceObj[prop].push(elem);
              }
            }
          } else {
            instanceObj[prop] = jsonProp;
          }
        }
    
        return instanceObj;
      }
    
      //#region ### get types ###
      /**
       * check type of value be string
       * @param {*} value
       */
      static isString(value: any) {
        return typeof value === "string" || value instanceof String;
      }
    
      /**
       * check type of value be array
       * @param {*} value
       */
      static isNumber(value: any) {
        return typeof value === "number" && isFinite(value);
      }
    
      /**
       * check type of value be array
       * @param {*} value
       */
      static isArray(value: any) {
        return value && typeof value === "object" && value.constructor === Array;
      }
    
      /**
       * check type of value be object
       * @param {*} value
       */
      static isObject(value: any) {
        return value && typeof value === "object" && value.constructor === Object;
      }
    
      /**
       * check type of value be boolean
       * @param {*} value
       */
      static isBoolean(value: any) {
        return typeof value === "boolean";
      }
      //#endregion
    }
    
    // #region ### Models ###
    class Hotel implements IDeserializable {
      id: number = 0;
      name: string = "";
      address: string = "";
      city: City = new City(); // complex data
      roomTypes: Array<RoomType> = []; // array of complex data
      facilities: Array<string> = []; // array of primitive data
    
      // getter example
      get nameAndAddress() {
        return `${this.name} ${this.address}`;
      }
    
      // function example
      checkRoom() {
        return true;
      }
    
      // this function will be use for getting run-time type information
      getTypes() {
        return {
          city: City,
          roomTypes: RoomType
        };
      }
    }
    
    class RoomType implements IDeserializable {
      id: number = 0;
      name: string = "";
      roomPrices: Array<RoomPrice> = [];
    
      // getter example
      get totalPrice() {
        return this.roomPrices.map(x => x.price).reduce((a, b) => a + b, 0);
      }
    
      getTypes() {
        return {
          roomPrices: RoomPrice
        };
      }
    }
    
    class RoomPrice {
      price: number = 0;
      date: string = "";
    }
    
    class City {
      id: number = 0;
      name: string = "";
    }
    // #endregion
    
    // #region ### test code ###
    var jsonObj = {
      id: 1,
      name: "hotel1",
      address: "address1",
      city: {
        id: 1,
        name: "city1"
      },
      roomTypes: [
        {
          id: 1,
          name: "single",
          roomPrices: [
            {
              price: 1000,
              date: "2020-02-20"
            },
            {
              price: 1500,
              date: "2020-02-21"
            }
          ]
        },
        {
          id: 2,
          name: "double",
          roomPrices: [
            {
              price: 2000,
              date: "2020-02-20"
            },
            {
              price: 2500,
              date: "2020-02-21"
            }
          ]
        }
      ],
      facilities: ["facility1", "facility2"]
    };
    
    var hotelInstance = Utility.deserializeJson<Hotel>(jsonObj, Hotel);
    
    console.log(hotelInstance.city.name);
    console.log(hotelInstance.nameAndAddress); // getter
    console.log(hotelInstance.checkRoom()); // function
    console.log(hotelInstance.roomTypes[0].totalPrice); // getter
    // #endregion
    
    
    0 讨论(0)
  • 2020-11-22 08:54

    For simple objects, I like this method:

    class Person {
      constructor(
        public id: String, 
        public name: String, 
        public title: String) {};
    
      static deserialize(input:any): Person {
        return new Person(input.id, input.name, input.title);
      }
    }
    
    var person = Person.deserialize({id: 'P123', name: 'Bob', title: 'Mr'});
    

    Leveraging the ability to define properties in the constructor lets it be concise.

    This gets you a typed object (vs all the answers that use Object.assign or some variant, which give you an Object) and doesn't require external libraries or decorators.

    0 讨论(0)
  • 2020-11-22 08:55

    My approach is slightly different. I do not copy properties into new instances, I just change the prototype of existing POJOs (may not work well on older browsers). Each class is responsible for providing a SetPrototypes method to set the prototoypes of any child objects, which in turn provide their own SetPrototypes methods.

    (I also use a _Type property to get the class name of unknown objects but that can be ignored here)

    class ParentClass
    {
        public ID?: Guid;
        public Child?: ChildClass;
        public ListOfChildren?: ChildClass[];
    
        /**
         * Set the prototypes of all objects in the graph.
         * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
         * @param pojo Plain object received from API/JSON to be given the class prototype.
         */
        private static SetPrototypes(pojo: ParentClass): void
        {
            ObjectUtils.SetPrototypeOf(pojo.Child, ChildClass);
            ObjectUtils.SetPrototypeOfAll(pojo.ListOfChildren, ChildClass);
        }
    }
    
    class ChildClass
    {
        public ID?: Guid;
        public GrandChild?: GrandChildClass;
    
        /**
         * Set the prototypes of all objects in the graph.
         * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
         * @param pojo Plain object received from API/JSON to be given the class prototype.
         */
        private static SetPrototypes(pojo: ChildClass): void
        {
            ObjectUtils.SetPrototypeOf(pojo.GrandChild, GrandChildClass);
        }
    }
    

    Here is ObjectUtils.ts:

    /**
     * ClassType lets us specify arguments as class variables.
     * (where ClassType == window[ClassName])
     */
    type ClassType = { new(...args: any[]): any; };
    
    /**
     * The name of a class as opposed to the class itself.
     * (where ClassType == window[ClassName])
     */
    type ClassName = string & {};
    
    abstract class ObjectUtils
    {
    /**
     * Set the prototype of an object to the specified class.
     *
     * Does nothing if source or type are null.
     * Throws an exception if type is not a known class type.
     *
     * If type has the SetPrototypes method then that is called on the source
     * to perform recursive prototype assignment on an object graph.
     *
     * SetPrototypes is declared private on types because it should only be called
     * by this method. It does not (and must not) set the prototype of the object
     * itself - only the protoypes of child properties, otherwise it would cause a
     * loop. Thus a public method would be misleading and not useful on its own.
     * 
     * https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript
     */
    public static SetPrototypeOf(source: any, type: ClassType | ClassName): any
    {
        let classType = (typeof type === "string") ? window[type] : type;
    
        if (!source || !classType)
        {
            return source;
        }
    
        // Guard/contract utility
        ExGuard.IsValid(classType.prototype, "type", <any>type);
    
        if ((<any>Object).setPrototypeOf)
        {
            (<any>Object).setPrototypeOf(source, classType.prototype);
        }
        else if (source.__proto__)
        {
            source.__proto__ = classType.prototype.__proto__;
        }
    
        if (typeof classType["SetPrototypes"] === "function")
        {
            classType["SetPrototypes"](source);
        }
    
        return source;
    }
    
    /**
     * Set the prototype of a list of objects to the specified class.
     * 
     * Throws an exception if type is not a known class type.
     */
    public static SetPrototypeOfAll(source: any[], type: ClassType): void
    {
        if (!source)
        {
            return;
        }
    
        for (var i = 0; i < source.length; i++)
        {
            this.SetPrototypeOf(source[i], type);
        }
    }
    }
    

    Usage:

    let pojo = SomePlainOldJavascriptObjectReceivedViaAjax;
    
    let parentObject = ObjectUtils.SetPrototypeOf(pojo, ParentClass);
    
    // parentObject is now a proper ParentClass instance
    
    0 讨论(0)
  • 2020-11-22 08:58

    I've created a tool that generates TypeScript interfaces and a runtime "type map" for performing runtime typechecking against the results of JSON.parse: ts.quicktype.io

    For example, given this JSON:

    {
      "name": "David",
      "pets": [
        {
          "name": "Smoochie",
          "species": "rhino"
        }
      ]
    }
    

    quicktype produces the following TypeScript interface and type map:

    export interface Person {
        name: string;
        pets: Pet[];
    }
    
    export interface Pet {
        name:    string;
        species: string;
    }
    
    const typeMap: any = {
        Person: {
            name: "string",
            pets: array(object("Pet")),
        },
        Pet: {
            name: "string",
            species: "string",
        },
    };
    

    Then we check the result of JSON.parse against the type map:

    export function fromJson(json: string): Person {
        return cast(JSON.parse(json), object("Person"));
    }
    

    I've left out some code, but you can try quicktype for the details.

    0 讨论(0)
  • 2020-11-22 09:02

    These are some quick shots at this to show a few different ways. They are by no means "complete" and as a disclaimer, I don't think it's a good idea to do it like this. Also the code isn't too clean since I just typed it together rather quickly.

    Also as a note: Of course deserializable classes need to have default constructors as is the case in all other languages where I'm aware of deserialization of any kind. Of course, Javascript won't complain if you call a non-default constructor with no arguments, but the class better be prepared for it then (plus, it wouldn't really be the "typescripty way").

    Option #1: No run-time information at all

    The problem with this approach is mostly that the name of any member must match its class. Which automatically limits you to one member of same type per class and breaks several rules of good practice. I strongly advise against this, but just list it here because it was the first "draft" when I wrote this answer (which is also why the names are "Foo" etc.).

    module Environment {
        export class Sub {
            id: number;
        }
    
        export class Foo {
            baz: number;
            Sub: Sub;
        }
    }
    
    function deserialize(json, environment, clazz) {
        var instance = new clazz();
        for(var prop in json) {
            if(!json.hasOwnProperty(prop)) {
                continue;
            }
    
            if(typeof json[prop] === 'object') {
                instance[prop] = deserialize(json[prop], environment, environment[prop]);
            } else {
                instance[prop] = json[prop];
            }
        }
    
        return instance;
    }
    
    var json = {
        baz: 42,
        Sub: {
            id: 1337
        }
    };
    
    var instance = deserialize(json, Environment, Environment.Foo);
    console.log(instance);
    

    Option #2: The name property

    To get rid of the problem in option #1, we need to have some kind of information of what type a node in the JSON object is. The problem is that in Typescript, these things are compile-time constructs and we need them at runtime – but runtime objects simply have no awareness of their properties until they are set.

    One way to do it is by making classes aware of their names. You need this property in the JSON as well, though. Actually, you only need it in the json:

    module Environment {
        export class Member {
            private __name__ = "Member";
            id: number;
        }
    
        export class ExampleClass {
            private __name__ = "ExampleClass";
    
            mainId: number;
            firstMember: Member;
            secondMember: Member;
        }
    }
    
    function deserialize(json, environment) {
        var instance = new environment[json.__name__]();
        for(var prop in json) {
            if(!json.hasOwnProperty(prop)) {
                continue;
            }
    
            if(typeof json[prop] === 'object') {
                instance[prop] = deserialize(json[prop], environment);
            } else {
                instance[prop] = json[prop];
            }
        }
    
        return instance;
    }
    
    var json = {
        __name__: "ExampleClass",
        mainId: 42,
        firstMember: {
            __name__: "Member",
            id: 1337
        },
        secondMember: {
            __name__: "Member",
            id: -1
        }
    };
    
    var instance = deserialize(json, Environment);
    console.log(instance);
    

    Option #3: Explicitly stating member types

    As stated above, the type information of class members is not available at runtime – that is unless we make it available. We only need to do this for non-primitive members and we are good to go:

    interface Deserializable {
        getTypes(): Object;
    }
    
    class Member implements Deserializable {
        id: number;
    
        getTypes() {
            // since the only member, id, is primitive, we don't need to
            // return anything here
            return {};
        }
    }
    
    class ExampleClass implements Deserializable {
        mainId: number;
        firstMember: Member;
        secondMember: Member;
    
        getTypes() {
            return {
                // this is the duplication so that we have
                // run-time type information :/
                firstMember: Member,
                secondMember: Member
            };
        }
    }
    
    function deserialize(json, clazz) {
        var instance = new clazz(),
            types = instance.getTypes();
    
        for(var prop in json) {
            if(!json.hasOwnProperty(prop)) {
                continue;
            }
    
            if(typeof json[prop] === 'object') {
                instance[prop] = deserialize(json[prop], types[prop]);
            } else {
                instance[prop] = json[prop];
            }
        }
    
        return instance;
    }
    
    var json = {
        mainId: 42,
        firstMember: {
            id: 1337
        },
        secondMember: {
            id: -1
        }
    };
    
    var instance = deserialize(json, ExampleClass);
    console.log(instance);
    

    Option #4: The verbose, but neat way

    Update 01/03/2016: As @GameAlchemist pointed out in the comments (idea, implementation), as of Typescript 1.7, the solution described below can be written in a better way using class/property decorators.

    Serialization is always a problem and in my opinion, the best way is a way that just isn't the shortest. Out of all the options, this is what I'd prefer because the author of the class has full control over the state of deserialized objects. If I had to guess, I'd say that all other options, sooner or later, will get you in trouble (unless Javascript comes up with a native way for dealing with this).

    Really, the following example doesn't do the flexibility justice. It really does just copy the class's structure. The difference you have to keep in mind here, though, is that the class has full control to use any kind of JSON it wants to control the state of the entire class (you could calculate things etc.).

    interface Serializable<T> {
        deserialize(input: Object): T;
    }
    
    class Member implements Serializable<Member> {
        id: number;
    
        deserialize(input) {
            this.id = input.id;
            return this;
        }
    }
    
    class ExampleClass implements Serializable<ExampleClass> {
        mainId: number;
        firstMember: Member;
        secondMember: Member;
    
        deserialize(input) {
            this.mainId = input.mainId;
    
            this.firstMember = new Member().deserialize(input.firstMember);
            this.secondMember = new Member().deserialize(input.secondMember);
    
            return this;
        }
    }
    
    var json = {
        mainId: 42,
        firstMember: {
            id: 1337
        },
        secondMember: {
            id: -1
        }
    };
    
    var instance = new ExampleClass().deserialize(json);
    console.log(instance);
    
    0 讨论(0)
提交回复
热议问题