Viktor A
Viktor A

Reputation: 146

Angular HttpClient is not returning quite the objects I expect

Lets say I have

class Foo {
    bar: string;

    public doSomething() {}

}

In my service I'm getting array of those from a server:

this.http.get<Foo[]>(...)

This works, I'm getting all the expected values. BUT when I take one of the objects I got and try to invoke doSomething() on it, I get an error saying that foo (which is of type Foo) doesn't have method doSomething().

It appears that while http.get correctly parsed JSON and assigned all the properties of my objects, it didn't bother with setting up proper prototypes for them, so essentially they are Foos in name only, they are missing all the methods of Foo.

Is that normal or am I doing something wrong? Thank you.

Upvotes: 5

Views: 1801

Answers (2)

Matjaz Hirsman
Matjaz Hirsman

Reputation: 344

You can also do it by inheritance like this:

this.http.get<Foo>(...)
    .pipe(Foo.serializeResponseMap());

class definition:

export class Foo extends SerializableMap {
    static instanceType = Foo;

    constructor() { super(); }

    bar: string;

    public doSomething() {}
}


class SerializableMap {
  static instanceType: any;

  static serializeResponseMap(): any {
    const createInstance = (r) => {
      return Object.assign(new this.instanceType(), r);
    };

    return map((respValue: any) => {
      if (Array.isArray(respValue)) {
        return respValue.map(r => createInstance(r));
      }
      return createInstance(respValue);
    });
  }
}

Upvotes: 0

Oscar Paz
Oscar Paz

Reputation: 18292

Effectively. In the end, you're just parsing JSON, and the result of parsing JSON are just plain JavaScript objects, never custom classes. this.http.get<Foo[]> is just a hint for the compiler, but the generic parameter does not really do anything. It doesn't convert one object in another, not does it give them types.

As an advice, you should never use classes when typecasting the result of calls to services or getting objects from localStorage, sessionStorage and such. You should use interfaces instead, without methods.

However, it is possible to achieve what you want, if you really need your objects to be of class Foo and have the doSomething method:

this.http.get<Foo[]>(...).do(items => items.forEach(item => Object.setPrototypeOf(item, Foo.prototype)));

This will give every object the correcto prototype. However, this has a performance penalty, as changing the prototype of created objects messes with browser optimization, so do it under your own risk.

Another option would be to give Foo a constructor:

class Foo {
    constructor(item: Foo) {
        Object.assign(this, item);
    }

    bar: string;

    public doSomething() {}
}

Now:

this.http.get<Foo[]>(...).pipe(map(items => items.map(item => new Foo(item)))).subscribe(...);

Upvotes: 11

Related Questions