Marshall Tigerus
Marshall Tigerus

Reputation: 3764

Getter Methods on Deserialized Objects in Typescript

We have a variety of Http calls to our services that return a collection of objects. In our angular application, typescript interprets these into the appropriate type if specified, for example Person:

export class Person{
  public firstName: string;
  public lastName: string;
  public salutation: string;
}

Let's say I want to be able to easily build a greeting string for this user, but I want to be a good programmer and do it once instead of multiple places. So I create a getter that will retrieve it:

export class Person{
  public firstName: string;
  public lastName: string;
  public salutation: string;
  public get greeting(): string {
      return salutation + " " + firstName + " " + lastName;
  }
}

However, when typescript deserialized it, it is missing the prototype of the greeting property (it is set to undefined). I get that typescript is mapping the JSON object to the class it has, and any missing fields/properties do not exist on the mapped object (returning undefined if called). What's the best way to fix this?

As an example of how we are calling the service:

this.authHttp.post(this.url, user, params).then(
  (res: Person) => {
      console.log(res);
      console.log(res.greeting);
  }
);

Upvotes: 4

Views: 1912

Answers (3)

Rodrigo
Rodrigo

Reputation: 2503

The response of HTTP calling doesn't generate objects of Person class. The response is at really a plain JavaScript object that obviouslly doesn't have the greeting() method of Person class.

In order to fix that, you can apply the following code:

http.post(...)
    .pipe(map(res => Object.assign(new Person(), res)))
    .subscribe(...);

The Object.assign(new Person(), res) function will copy ALL the values of res to the Person instance. After this map, you will have a real instance of Person with its properties values copy from the resource returned by the http calling.

Upvotes: 1

Ian MacDonald
Ian MacDonald

Reputation: 14010

What you had worked by coincidence because the property names you were accessing were also provided by the response from the .post. In reality, TypeScript is no longer used after the tsc command which transpiles it to JavaScript source. Because of this, you lose all type-checking at runtime.

If you were to hard-code this assignment, like so:

let me: Person = { firstName: 'Ian', lastName: 'MacDonald', salutation: 'Hello.' };

you will receive a TypeScript error as it attempts to create the JavaScript.

Property 'greeting' is missing in type
    '{ firstName: string; lastName: string; salutation: string; }'
    but required in type 'Person'.ts(2741)

The same error will appear for any functions that you have defined on your class, as well. This is because { ... } is not your type; it only has whatever is explicitly defined. What comes down as your server response will be subject to the same assignment rules, but the issue won't be present (because the data isn't present) until run-time.

I recommend using a pipe to construct yourself an instance of your class before anything attempts to use the server response so that any errors can be caught at first contact instead of 10s later when you finally try to access the .save() action that doesn't exist.

.post(...).pipe(map((incoming: any) => {
  let person: Person = new Person();
  person.firstName = incoming.firstName;
  person.lastName = incoming.lastName;
  person.salutation = incoming.salutation;
  return person;
});

Upvotes: 1

Ingo Bürk
Ingo Bürk

Reputation: 20033

You have to create what's called a copy constructor that takes in all the fields of the object:

constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
}

Now you can do

http.post(...)
    .pipe(map(res => new Person(res.firstName, res.lastName)))
    .subscribe(...);

There's many variants of this, of course. You can just do

constructor(public firstName: string, public lastName: string) {}

to avoid manually assigning the values again, for example. Or think of any other way you'd like to write this. It all comes down to the same, which is that you need to actually create an instance of your class rather than using the class type only as a type.

Upvotes: 1

Related Questions