在Typescript中从不同类型的json中加载对象数组的最佳方法是什么?

fhity93d  于 2023-01-06  发布在  TypeScript
关注(0)|答案(1)|浏览(120)

我想用typescript做reflection,我有这样的结构:

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}
];

我找到了一个泛型方法来通过反射加载这个json,与类类型(在typeName属性中定义)有关。
因此,结果必须是一个数组,其中第一个元素的类型为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);
ibrsph3r

ibrsph3r1#

现在,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制作一个 Package 器(假设您正在发出某种API请求来获取数据)-坦白地说,无论如何您都应该 Package 它,这样做有很多好处。
因此,您的服务将使用新的HttpService,而不是直接使用HttpClient,这为您提供了一个在将响应返回给调用服务之前更改响应(水合响应)的位置。
带着一点怀疑,这样的东西会从它那里产生:

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

差不多吧。我不知道,活下去的意志在消退。
我将垃圾邮件的最后一件事,因为它的标准有效地复制相关内容从任何链接的情况下,链接死亡,所以这里的水合物函数从那里:

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的世界里,接口完成了这项工作,因为你无论如何都得不到对象类型的物理示例。使用helper/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);

试试看?

相关问题