【问题标题】:How is the best way to load an array of objects from json of differentes types in Typescript?在 Typescript 中从不同类型的 json 加载对象数组的最佳方法是什么?
【发布时间】:2023-01-04 16:28:30
【问题描述】:

我想要做反射用打字稿。我有这样的结构:

class Base {
  public id: number;
  public typeName: string; // name of this type
}

class Extend1 extends Base {
  public foo: string;
}

public Extend2 extends Base {
  public someProperty: boolean;
}

所以我有这个 json 由 HttpClient 得到:

const json = [
  {id: 1, typeName: "Extend1", name: "toto"},
  {id: 2, typeName: "Extend2", someProperty: true}
];

关于类类型(在 typeName 属性中定义),我找到了一种通过反射加载此 json 的通用方法。

因此结果必须是一个数组,其中第一个元素是 Extend1 类型,第二个元素是 Extend2 类型。

就像是 :

const myArray : Base[] = loadFromJson(json);

myArray[0] instanceof Extend1; // true;
myArray[1] instanceof Extend2; // true;

是什么最好怎么做?

OBS:我不想像这样进行切换:

switch(json.typeName) {
  case 'Extend1': return new Extend1();
  break;
}

return Object.createInstance(json.typeName);

【问题讨论】:

    标签: angular typescript reflection


    【解决方案1】:

    现在,TS 在类型、对象、接口和 JSON 反序列化方面很时髦。

    让我们快速回顾一下记忆通道(对我来说)。

    我们有一些对象,因为,你知道,OOP。

    export class MyObject {
        public someProperty: string;
    
        public AddSomething(str: string): void {
            this.someProperty += str;
        }
    }
    

    多么令人愉快的小物件。

    当我们从 API 得到响应时,似乎一切都很好;

    [
        {"someProperty": "hello"},
        {"someProperty": "world"}
    ]
    

    但是当我们用我们的对象做事并想利用提供的超级有用的方法时......

    this.data.forEach((x: MyObject) => {
        x.addSomething('!');
    });
    

    ...它告诉我们对象上没有这样的方法addSomething

    唔?

    所以,事情是这样的:类型是一种幻觉。

    TS/JS 只是将 json 解析为一个基本对象,一个东西的字典。例如,与 C# 不同的是,反序列化某些东西实际上会创建该对象的实例,例如:

    var result = JsonConvert.DeserialiseObject<IEnumerable<MyObject>>(str);
    

    result 列表中的每一项都是一个具体的 MyObject 对象。在引擎盖下,它做了它的整个反射事情和诸如此类的事情,并且在物理上做了一个new MyObject()

    在 TS 中,它只是一个字典/对象,不是一个具体的MyObject对象,所以它只是:

    {someProperty: 'hello'}
    

    并不是:

    {someProperty: 'hello', addSomething: (str: string) => {...}}
    

    所以,最终这就是你问的问题。如何推出自己的自动 C# 类 JSON 解析水化机制。

    我们选择对我们的对象做一件相当奇怪的事情,那就是给他们时髦的构造函数:

    export class MyObject {
        public someProperty: string;
    
        constructor(obj?: any) {
            if (!obj) return;
            this.someProperty = obj.someProperty;
        }
    }
    

    这样我们就可以做到:

    const concreteObjects: MyObject[] = [];
    this.data.forEach((x: MyObject /*ButNotARealMyObject*/) => {
        concreteObjects.push(new MyObject(x));
    });
    

    现在我们已经从无生命的属性包转换为我们可以使用的实际对象类型。万岁!

    另一种方法是做一些更智能和可重用的事情。 this 之类的东西看起来很有前途......

    看起来是一个有效使用“反射”的帮手——在 JS/TS 中只是在做Object.keys(someObj)——用一些我不知道存在的时髦语法/机制来遍历对象具有的所有属性,因为我还没有以前关心创建给定类型的实例。

    仍然有点手动,因为一旦您收到回复,您需要输入const data = HydrationHelper.hydrate(MyObject, jsonData); 或其他任何内容。

    每当您需要手动重复做某事时,这意味着您可以自动完成!

    让自己成为 HttpClient 的包装器(假设您正在发出某种 API 请求来获取您的数据)——坦率地说,无论如何您都应该包装它,这样做有很多好处。

    因此,您的服务不会直接使用HttpClient,而是使用您的新HttpService,这样您就可以在将响应返回给调用服务之前更改响应(补充您的响应)。

    加上一小撮盐,就会产生这样的东西:

    export class HttpService {
        constructor(private readonly http: HttpClient) { }
    
        public get<T>(url: string, queryParams: object): Observable<T> {
            url = parseParams(url, queryParams);
            this.http.get(url)
                .pipe(map( /*map might not be the right function, I don't have my IDE up to make sure and at this point I want to go to bed*/
                    (x) => {
                        return HydrationHelper.hydrate(T, x);   
                    }
                )
            );
        }
    }
    

    像那样的东西。我不知道,会不会活着褪色。

    我将最后一件事发送垃圾邮件,因为在链接失效的情况下有效地从任何链接复制相关内容是标准的,所以这里是 hydrate 函数:

    function hydrate<T>(constr: { new(...args: any[]): T }, data: string, strictMode: boolean = true, ...args: any[]): T {
        const obj = JSON.parse(data);
        const instance = new constr(...args);
    
        for (let key in obj) {
            if (!strictMode || instance.hasOwnProperty(key)) {
                instance[key] = obj[key];
            }
        }
    
        return instance;
    }
    

    我对 HydrationHelper 的模糊引用将把它从 js 函数变成更打字的东西。我不知道为什么我选择静态类而不是可注入服务,这是明智的,但无论如何,它说明了这一点。

    export class HydrationHelper {
        public static hydrate<T>(constr: { new(...args: any[]): T }, data: string, strictMode: boolean = true, ...args: any[]): T {
            const obj = JSON.parse(data);
            const instance = new constr(...args);
    
            for (let key in obj) {
                if (!strictMode || instance.hasOwnProperty(key)) {
                    instance[key] = obj[key];
                }
            }
    
            return instance;
        }
    }
    

    哦,实际上,最后一件事——我的第一句话提到了接口,我没有做过。

    我们不再为我们的基本 dto 打扰类。接口在 TS 的世界中发挥作用,因为无论如何您都无法获得对象类型的物理实例。使用 helpers/services/whatever 或者,是的,做所有这些额外的工作以获得正确的类型,以便使用各种函数来操作您的数据。

    没有内部函数,这个接口和类一样好,你不会遇到任何反序列化混淆:

    export interface MyObject {
        someProperty: string;
    }
    

    编辑:稍后进行一些挖掘,看起来 TS 确实具有与 C# 相同的能力,能够从字符串中获取类型——只要字符串是确切的类型名称。

    const typeName = 'number'; 
    type myType = typeof typeName; 
    const x: myType = 5;
    

    因此,鉴于此,稍微改变我的第一个垃圾邮件,我认为你应该能够做类似的事情:

    type responseType = typeof response.typeName;
    HydrationHelper.hydrate(responseType, response);
    

    试试看?

    【讨论】:

    • 非常感谢您提供这么长而明确的答复。用完美的话来说,这可以完美地工作。但就我而言,不!这些对象可以具有其他类型的其他对象的属性和/或其他类型的对象数组。所以,默认情况下我不知道我将从 API 获得什么样的对象。
    • 您不知道数据将采用什么形式?你只是在玩猜谜游戏?获得数据后,您希望如何处理这些数据?
    • 是的,我有。购买它可以有许多不同的类型,我必须知道必须创建什么类型的唯一方法是列“typeName”,其中包含必须创建的类的名称。
    猜你喜欢
    • 2017-02-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-11
    • 2020-10-30
    • 2019-08-28
    • 2011-09-18
    相关资源
    最近更新 更多