我如何初始化一个TypeScript对象与json对象?

我从对REST服务器的AJAX调用中接收到一个JSON对象。这个对象的属性名与我的TypeScript类(这是这个问题的后续)相匹配。

初始化它的最佳方法是什么?我不认为将工作,因为类(&JSON对象)的成员是对象的列表,成员是类,这些类的成员是列表和/或类。

但我更喜欢一种方法,查找成员名和分配他们,创建列表和实例化类的需要,所以我不必为每个类中的每个成员写显式代码(有很多!)

451744 次浏览

这是一些快速的镜头,展示了几种不同的方法。他们绝不是“完整的”,作为一个免责声明,我不认为这样做是一个好主意。此外,代码不是太干净,因为我只是键入它在一起相当快。

另外需要注意的是:当然,可反序列化的类需要有默认构造函数,就像我所知道的所有其他语言中的反序列化一样。当然,如果你调用一个不带参数的非默认构造函数,Javascript不会抱怨,但是类最好为此做好准备(另外,它不是真正的“typescript方式”)。

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

这种方法的问题主要在于任何成员的名称必须与其类匹配。它自动地限制你每个类只能有一个相同类型的成员,并打破了一些良好实践的规则。我强烈反对这样做,但这里只列出它,因为这是我写这个答案时的第一份“草稿”(这也是为什么名字是“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:冗长但简洁的方式

正如@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);

上面描述的第4个选项是一种简单而漂亮的方法,它必须与第二个选项相结合,在这种情况下,您必须处理一个类层次结构,例如成员列表,它是成员超类的任何一个子类,例如Director extends member或Student extends member。在这种情况下,你必须以json格式给出子类类型

TLDR: TypedJSON(概念工作证明)


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

幸运的是,这可以用修饰符ReflectDecorators以一种非常优雅和健壮的方式解决:

  1. 在需要序列化的属性上使用属性修饰符来记录元数据信息,并将该信息存储在某个地方,例如类原型上
  2. 将此元数据信息提供给递归初始化器(反序列化器)

,

记录类型信息

通过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__中的整个实例(上面收集的),并相应地实例化所需的属性。必须指定根对象的类型,以便反序列化程序有一个起始点。

同样,这个方法的一个非常简单的实现是:

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。

,

边界情况

然而,如果你现在很高兴解决方案是简单,我有一些坏消息:有巨大的数量的边缘情况需要照顾。只有一些是:

  • 数组和数组元素(特别是在嵌套数组中)
  • 多态性
  • 抽象类和接口
  • ...

如果你不想摆弄所有这些(我打赌你不想),我很高兴推荐一个使用这种方法的概念证明的工作实验版本,TypedJSON——我创建它是为了解决这个确切的问题,一个我每天都要面对的问题。

由于decorator仍被认为是实验性的,我不建议在生产中使用它,但到目前为止它对我来说很有用。

也许不现实,但简单的解决方案:

interface Bar{
x:number;
y?:string;
}


var baz:Bar = JSON.parse(jsonString);
alert(baz.y);

对困难的依赖也要努力!!

JQuery .extend为你做这个:

var mytsobject = new mytsobject();


var newObj = {a:1,b:2};


$.extend(mytsobject, newObj); //mytsobject will now contain a & b

我不知道这是什么时候添加的,我目前使用的是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

你可以看到它没有递归地赋值

你可以像下面这样做

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

而且

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

选项#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()
}

我一直在使用这个家伙来完成工作: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);

另一种选择是使用工厂

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. 保持类简单
  2. 工厂可灵活注射

我已经创建了一个工具,它生成TypeScript接口和一个运行时“类型映射”,用于对JSON.parse: ts.quicktype.io的结果执行运行时类型检查

例如,给定这个JSON:

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

quicktype生成以下TypeScript接口和类型映射:

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来了解细节。

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

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

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的答案相比)。赋值或一些变量,给你一个对象),不需要外部库或装饰器。

我找到的最好的方法是class-transformer

这就是它的用法:

一些类:

export class Foo {


name: string;


@Type(() => Bar)
bar: Bar;


public someFunction = (test: string): boolean => {
...
}
}


// the docs say "import [this shim] in a global place, like app.ts"
import 'reflect-metadata';


// import this function where you need to use it
import { plainToClass } from 'class-transformer';


export class SomeService {


anyFunction() {
u = plainToClass(Foo, JSONobj);
}
}

如果你使用@Type装饰器,嵌套属性也会被创建。

我个人更喜欢@Ingo的选项3 Bürk。 我改进了他的代码,以支持复杂数据数组和基本数据数组

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


我的方法略有不同。我没有将属性复制到新的实例中,我只是改变了现有pojo的原型(在旧的浏览器上可能不太好用)。每个类负责提供一个setprototype方法来设置任何子对象的原型,这些子对象反过来提供它们自己的setprototype方法。

(我也使用_Type属性来获取未知对象的类名,但在这里可以忽略)

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

下面是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);
}
}
}

用法:

let pojo = SomePlainOldJavascriptObjectReceivedViaAjax;


let parentObject = ObjectUtils.SetPrototypeOf(pojo, ParentClass);


// parentObject is now a proper ParentClass instance

这是我的方法(非常简单):

const jsonObj: { [key: string]: any } = JSON.parse(jsonStr);


for (const key in jsonObj) {
if (!jsonObj.hasOwnProperty(key)) {
continue;
}


console.log(key); // Key
console.log(jsonObj[key]); // Value
// Your logic...
}

如果你想要类型安全,不喜欢装饰器

abstract class IPerson{
name?: string;
age?: number;
}
class Person extends IPerson{
constructor({name, age}: IPerson){
super();
this.name = name;
this.age = age;
}
}


const json = {name: "ali", age: 80};
const person = new Person(json);

或者我更喜欢这个

class Person {
constructor(init?: Partial<Person>){
Object.assign(this, init);
}
name?: string;
age?: number;
}


const json = {name: "ali", age: 80};
const person = new Person(json);