How do I cast a JSON object to a typescript class

2018-12-31 04:11发布

I read a JSON object from a remote REST server. This JSON object has all the properties of a typescript class (by design). How do I cast that received JSON object to a type var?

I don't want to populate a typescript var (ie have a constructor that takes this JSON object). It's large and copying everything across sub-object by sub-object & property by property would take a lot of time.

Update: You can however cast it to a typescript interface!

16条回答
临风纵饮
2楼-- · 2018-12-31 04:56

There is nothing yet to automatically check if the JSON object you received from the server has the expected (read is conform to the) typescript's interface properties. But you can use User-Defined Type Guards

Considering the following interface and a silly json object (it could have been any type):

interface MyInterface {
    key: string;
 }

const json: object = { "key": "value" }

Three possible ways:

A. Type Assertion or simple static cast placed after the variable

const myObject: MyInterface = json as MyInterface;

B. Simple static cast, before the variable and between diamonds

const myObject: MyInterface = <MyInterface>json;

C. Advanced dynamic cast, you check yourself the structure of the object

function isMyInterface(json: any): json is MyInterface {
    // silly condition to consider json as conform for MyInterface
    return typeof json.key === "string";
}

if (isMyInterface(json)) {
    console.log(json.key)
}
else {
        throw new Error(`Expected MyInterface, got '${json}'.`);
}

You can play with this example here

Note that the difficulty here is to write the isMyInterface function. I hope TS will add a decorator sooner or later to export complex typing to the runtime and let the runtime check the object's structure when needed. For now, you could either use a json schema validator which purpose is approximately the same OR this runtime type check function generator

查看更多
唯独是你
3楼-- · 2018-12-31 04:57

In TypeScript you can do a type assertion using an interface and generics like so:

var json = Utilities.JSONLoader.loadFromFile("../docs/location_map.json");
var locations: Array<ILocationMap> = JSON.parse(json).location;

Where ILocationMap describes the shape of your data. The advantage of this method is that your JSON could contain more properties but the shape satisfies the conditions of the interface.

I hope that helps!

查看更多
浪荡孟婆
4楼-- · 2018-12-31 05:01

TLDR: One liner

// This assumes your constructor method will assign properties from the arg.
.map((instanceData: MyClass) => new MyClass(instanceData));

The Detailed Answer

I would not recommend the Object.assign approach, as it can inappropriately litter your class instance with irrelevant properties (as well as defined closures) that were not declared within the class itself.

In the class you are trying to deserialize into, I would ensure any properties you want deserialized are defined (null, empty array, etc). By defining your properties with initial values you expose their visibility when trying to iterate class members to assign values to (see deserialize method below).

export class Person {
  public name: string = null;
  public favoriteSites: string[] = [];

  private age: number = null;
  private id: number = null;
  private active: boolean;

  constructor(instanceData?: Person) {
    if (instanceData) {
      this.deserialize(instanceData);
    }
  }

  private deserialize(instanceData: Person) {
    // Note this.active will not be listed in keys since it's declared, but not defined
    const keys = Object.keys(this);

    for (const key of keys) {
      if (instanceData.hasOwnProperty(key)) {
        this[key] = instanceData[key];
      }
    }
  }
}

In the example above, I simply created a deserialize method. In a real world example, I would have it centralized in a reusable base class or service method.

Here is how to utilize this in something like an http resp...

this.http.get(ENDPOINT_URL)
  .map(res => res.json())
  .map((resp: Person) => new Person(resp) ) );

If tslint/ide complains about argument type being incompatible, just cast the argument into the same type using angular brackets <YourClassName>, example:

const person = new Person(<Person> { name: 'John', age: 35, id: 1 });

If you have class members that are of a specific type (aka: instance of another class), then you can have them casted into typed instances through getter/setter methods.

export class Person {
  private _acct: UserAcct = null;
  private _tasks: Task[] = [];

  // ctor & deserialize methods...

  public get acct(): UserAcct {
    return this.acct;
  }
  public set acct(acctData: UserAcct) {
    this._acct = new UserAcct(acctData);
  }

  public get tasks(): Task[] {
    return this._tasks;
  }

  public set tasks(taskData: Task[]) {
    this._tasks = taskData.map(task => new Task(task));
  }
}

The above example will deserialize both acct and the list of tasks into their respective class instances.

查看更多
何处买醉
5楼-- · 2018-12-31 05:02

If you are using ES6, try this:

class Client{
  name: string

  displayName(){
    console.log(this.name)
  }
}

service.getClientFromAPI().then(clientData => {

  // Here the client data from API only have the "name" field
  // If we want to use the Client class methods on this data object we need to:
  let clientWithType = Object.assign(new Client(), clientData)

  clientWithType.displayName()
})

But this way will not work on the nest object, sadly.

查看更多
登录 后发表回答