首页 文章

如何使用JSON对象初始化TypeScript对象

提问于
浏览
149

我从AJAX调用REST服务器接收JSON对象 . 此对象具有与我的TypeScript类匹配的属性名称(这是this question的后续内容) .

初始化它的最佳方法是什么?我认为this不会起作用,因为类(&JSON对象)具有作为对象列表的成员和作为类的成员,并且这些类具有作为列表和/或类的成员 .

但我更喜欢一种查找成员名称并将其分配的方法,根据需要创建列表并实例化类,因此我不必为每个类中的每个成员编写显式代码(有很多!)

13 回答

  • 0
    **model.ts**
    export class Item {
        private key: JSON;
        constructor(jsonItem: any) {
            this.key = jsonItem;
        }
    }
    
    **service.ts**
    import { Item } from '../model/items';
    
    export class ItemService {
        items: Item;
        constructor() {
            this.items = new Item({
                'logo': 'Logo',
                'home': 'Home',
                'about': 'About',
                'contact': 'Contact',
            });
        }
        getItems(): Item {
            return this.items;
        }
    }
    
  • 157

    你现在可以使用 Object.assign 我当前正在使用Typescript 2.0.2,这似乎是一个ES6功能 .

    client.fetch( '' ).then( response => {
            return response.json();
        } ).then( json => {
            let hal : HalJson = Object.assign( new HalJson(), json );
            log.debug( "json", hal );
    

    这是 HalJson

    export class HalJson {
        _links: HalLinks;
    }
    
    export class HalLinks implements Links {
    }
    
    export interface Links {
        readonly [text: string]: Link;
    }
    
    export interface Link {
        readonly href: URL;
    }
    

    这就是chrome说的

    HalJson {_links: Object}
    _links
    :
    Object
    public
    :
    Object
    href
    :
    "http://localhost:9000/v0/public
    

    所以你可以看到它没有递归地分配

  • -1

    你可以在下面做

    export interface Instance {
      id?:string;
      name?:string;
      type:string;
    }
    

    var instance: Instance = <Instance>({
          id: null,
          name: '',
          type: ''
        });
    
  • 0

    这些是一些快速拍摄,以显示几种不同的方式 . 它们绝不是“完整的”,作为免责声明,我不认为这样做是个好主意 . 此外,代码不是太干净,因为我只是很快地将它们拼凑在一起 .

    另外作为注释:当然,反序列化的类需要有默认的构造函数,就像我知道任何类型的反序列化的所有其他语言一样 . 当然,如果你调用一个没有参数的非默认构造函数,Javascript就不会抱怨,但是那个类更好地为它做好准备(而且,它实际上不是“typescripty方式”) .

    选项#1:根本没有运行时信息

    这种方法的问题主要是任何成员的名称必须与其类匹配 . 这会自动将您限制为每个 class 相同类型的一名成员,并违反了一些良好做法规则 . 我强烈反对这一点,但只是在这里列出,因为这是我写这个答案时的第一个“草稿”(这也是为什么名字是“Foo”等) .

    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);
    

    选项#2:名称属性

    为了摆脱选项#1中的问题,我们需要获得JSON对象中节点类型的某种信息 . 问题是在Typescript中,这些东西是编译时构造,我们在运行时需要它们 - 但运行时对象在设置之前根本不知道它们的属性 .

    一种方法是让类知道他们的名字 . 但是,您也需要在JSON中使用此属性 . 实际上,你只需要在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);
    

    选项#3:明确说明成员类型

    如上所述,类成员的类型信息在运行时不可用 - 除非我们使其可用 . 我们只需要为非原始成员执行此操作,我们很乐意去:

    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);
    

    选项#4:冗长而又整洁的方式

    2016年1月3日更新:正如@GameAlchemist在评论中指出的那样,从Typescript 1.7开始,下面描述的解决方案可以使用类/属性装饰器以更好的方式编写 .

    序列化总是一个问题,在我看来,最好的方法是一种不是最短的方式 . 在所有选项中,这是我更喜欢的,因为类的作者可以完全控制反序列化对象的状态 . 如果我不得不猜测,我会说所有其他选项,迟早会让你遇到麻烦(除非Javascript提出了一个本地方式来解决这个问题) .

    实际上,下面的例子没有做到灵活正义 . 它确实只是复制了类的结构 . 但是,你必须要记住的是,该类可以完全控制使用它想要控制整个类状态的任何类型的JSON(你可以计算等等) .

    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

    TLDR: TypedJSON (working proof of concept)


    这个问题的复杂性的根源是我们需要在运行时使用仅在编译时存在的类型信息来反序列化JSON . 这要求类型信息以某种方式在运行时可用 .

    幸运的是,这可以通过decoratorsReflectDecorators以非常优雅和强大的方式解决:

    • 对要进行序列化的属性使用property decorators,以记录元数据信息并将该信息存储在某处,例如在类原型上

    • 将此元数据信息提供给递归初始值设定项(解串器)

    录制类型 - 信息

    通过ReflectDecorators和属性装饰器的组合,可以轻松记录关于属性的类型信息 . 这种方法的基本实施将是:

    function JsonMember(target: any, propertyKey: string) {
        var metadataFieldKey = "__propertyTypes__";
    
        // Get the already recorded type-information from target, or create
        // empty object if this is the first property.
        var propertyTypes = target[metadataFieldKey] || (target[metadataFieldKey] = {});
    
        // Get the constructor reference of the current property.
        // This is provided by TypeScript, built-in (make sure to enable emit
        // decorator metadata).
        propertyTypes[propertyKey] = Reflect.getMetadata("design:type", target, propertyKey);
    }
    

    对于任何给定的属性,上面的代码片段会将属性的构造函数的引用添加到类原型上的隐藏 __propertyTypes__ 属性 . 例如:

    class Language {
        @JsonMember // String
        name: string;
    
        @JsonMember// Number
        level: number;
    }
    
    class Person {
        @JsonMember // String
        name: string;
    
        @JsonMember// Language
        language: Language;
    }
    

    就是这样,我们在运行时拥有所需的类型信息,现在可以对其进行处理 .

    处理类型 - 信息

    我们首先需要使用 JSON.parse 获取 Object 实例 - 之后,我们可以迭代 __propertyTypes__ (上面收集的)中的entires并相应地实例化所需的属性 . 必须指定根对象的类型,以便反序列化器具有起始点 .

    同样,这种方法的简单实现将是:

    function deserialize<T>(jsonObject: any, Constructor: { new (): T }): T {
        if (!Constructor || !Constructor.prototype.__propertyTypes__ || !jsonObject || typeof jsonObject !== "object") {
            // No root-type with usable type-information is available.
            return jsonObject;
        }
    
        // Create an instance of root-type.
        var instance: any = new Constructor();
    
        // For each property marked with @JsonMember, do...
        Object.keys(Constructor.prototype.__propertyTypes__).forEach(propertyKey => {
            var PropertyType = Constructor.prototype.__propertyTypes__[propertyKey];
    
            // Deserialize recursively, treat property type as root-type.
            instance[propertyKey] = deserialize(jsonObject[propertyKey], PropertyType);
        });
    
        return instance;
    }
    
    var json = '{ "name": "John Doe", "language": { "name": "en", "level": 5 } }';
    var person: Person = deserialize(JSON.parse(json), Person);
    

    以上的想法具有通过预期类型(对于复杂/对象值)反序列化的巨大优势,而不是JSON中存在的类型 . 如果需要 Person ,则它是创建的 Person 实例 . 通过对原始类型和数组采取一些额外的安全措施,可以使这种方法安全,抵御任何恶意JSON .

    边缘案例

    但是,如果您现在对解决方案很简单感到高兴,我会有一些坏消息:需要处理大量边缘情况 . 只有一部分是:

    • 数组和数组元素(特别是在嵌套数组中)

    • 多态性

    • 抽象类和接口

    • ......

    如果你不愿意,我很乐意推荐一个使用这种方法的概念验证的实验版本,我为解决这个问题而创建了这个问题,这是我每天面对的一个问题 .

    由于装饰器仍然被认为是实验性的,我不建议将它用于 生产环境 用途,但到目前为止它对我很有帮助 .

  • 26

    我一直用这个人来做这个工作:https://github.com/weichx/cerialize

    它非常简单但功能强大 . 它支持:

    • 整个对象树的序列化和反序列化 .

    • 同一对象上的持久性和瞬态属性 .

    • 用于自定义(反)序列化逻辑的挂钩 .

    • 它可以(反)序列化为现有实例(非常适合Angular)或生成新实例 .

    例:

    class Tree {
      @deserialize public species : string; 
      @deserializeAs(Leaf) public leafs : Array<Leaf>;  //arrays do not need extra specifications, just a type.
      @deserializeAs(Bark, 'barkType') public bark : Bark;  //using custom type and custom key name
      @deserializeIndexable(Leaf) public leafMap : {[idx : string] : Leaf}; //use an object as a map
    }
    
    class Leaf {
      @deserialize public color : string;
      @deserialize public blooming : boolean;
      @deserializeAs(Date) public bloomedAt : Date;
    }
    
    class Bark {
      @deserialize roughness : number;
    }
    
    var json = {
      species: 'Oak',
      barkType: { roughness: 1 },
      leafs: [ {color: 'red', blooming: false, bloomedAt: 'Mon Dec 07 2015 11:48:20 GMT-0500 (EST)' } ],
      leafMap: { type1: { some leaf data }, type2: { some leaf data } }
    }
    var tree: Tree = Deserialize(json, Tree);
    
  • 10

    我创建了一个生成TypeScript接口的工具和一个运行时"type map",用于对 JSON.parse 的结果执行运行时类型检查:ts.quicktype.io

    例如,给定此JSON:

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

    quicktype生成以下TypeScript接口并键入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",
        },
    };
    

    然后我们针对类型映射检查 JSON.parse 的结果:

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

    我遗漏了一些代码,但您可以尝试quicktype了解详细信息 .

  • 29

    选项#5:使用Typescript构造函数和jQuery.extend

    这似乎是最易于维护的方法:添加一个构造函数,该构造函数将json结构作为参数,并扩展json对象 . 这样您就可以将json结构解析为整个应用程序模型 .

    无需在构造函数中创建接口或列出属性 .

    export class Company
    {
        Employees : Employee[];
    
        constructor( jsonData: any )
        {
            jQuery.extend( this, jsonData);
    
            // apply the same principle to linked objects:
            if ( jsonData.Employees )
                this.Employees = jQuery.map( jsonData.Employees , (emp) => {
                    return new Employee ( emp );  });
        }
    
        calculateSalaries() : void { .... }
    }
    
    export class Employee
    {
        name: string;
        salary: number;
        city: string;
    
        constructor( jsonData: any )
        {
            jQuery.extend( this, jsonData);
    
            // case where your object's property does not match the json's:
            this.city = jsonData.town;
        }
    }
    

    在您收到公司计算工资的ajax回调中:

    onReceiveCompany( jsonCompany : any ) 
    {
       let newCompany = new Company( jsonCompany );
    
       // call the methods on your newCompany object ...
       newCompany.calculateSalaries()
    }
    
  • 2

    上面描述的第四个选项是一个简单而好的方法,在必须处理类层次结构的情况下必须与第二个选项结合,例如成员列表,它是任何子类的出现 . 会员超级会员,例如,会员延伸会员或学生延伸会员 . 在这种情况下,您必须以json格式提供子类类型

  • 3

    也许不是实际的,但简单的解决方案

    interface Bar{
    x:number;
    y?:string; 
    }
    
    var baz:Bar = JSON.parse(jsonString);
    alert(baz.y);
    

    为困难的依赖工作!

  • 0

    JQuery .extend为您做到这一点:

    var mytsobject = new mytsobject();
    
    var newObj = {a:1,b:2};
    
    $.extend(mytsobject, newObj); //mytsobject will now contain a & b
    
  • 1

    另一种选择使用工厂

    export class A {
    
        id: number;
    
        date: Date;
    
        bId: number;
        readonly b: B;
    }
    
    export class B {
    
        id: number;
    }
    
    export class AFactory {
    
        constructor(
            private readonly createB: BFactory
        ) { }
    
        create(data: any): A {
    
            const createB = this.createB.create;
    
            return Object.assign(new A(),
                data,
                {
                    get b(): B {
    
                        return createB({ id: data.bId });
                    },
                    date: new Date(data.date)
                });
        }
    }
    
    export class BFactory {
    
        create(data: any): B {
    
            return Object.assign(new B(), data);
        }
    }
    

    https://github.com/MrAntix/ts-deserialize

    像这样用

    import { A, B, AFactory, BFactory } from "./deserialize";
    
    // create a factory, simplified by DI
    const aFactory = new AFactory(new BFactory());
    
    // get an anon js object like you'd get from the http call
    const data = { bId: 1, date: '2017-1-1' };
    
    // create a real model from the anon js object
    const a = aFactory.create(data);
    
    // confirm instances e.g. dates are Dates 
    console.log('a.date is instanceof Date', a.date instanceof Date);
    console.log('a.b is instanceof B', a.b instanceof B);
    
    • 使您的课程变得简单

    • 注射可供工厂灵活使用

  • -1

    对于简单对象,我喜欢这种方法:

    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'});
    

    利用在构造函数中定义属性的能力使其简洁 .

    这会为您提供一个类型化对象(与使用Object.assign或某些变体的所有答案相比,它们会为您提供一个Object)并且不需要外部库或装饰器 .

相关问题