Tarmo
Tarmo

Reputation: 4081

Can't map Angular5 HttpClient responses to my typescript classes

I have a problem mapping my Angular 5 HttpClient responses to concrete TypeScript classes.

I have a following Angular 5 class:

export class MaintainableUser {

  constructor(public id: string, public name: string, public privileges: string[]) {
  }

  public hasPrivilege(privilege: string): boolean {
    return this.privileges.indexOf(privilege) != -1;
  }

  public togglePrivilege(privilege: string) {
    if (this.hasPrivilege(privilege)) {
      this.privileges.splice(this.privileges.indexOf(privilege), 1);
    } else {
      this.privileges.push(privilege);
    }
    console.log(this.privileges);
  }
}

I have a following Angular5 service:

import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {MaintainableUser} from './maintain-users/maintainable-user';
import {Observable} from 'rxjs/Observable';
import {catchError} from 'rxjs/operators';
import {ApiErrorHandler} from '../api-error-handler';

@Injectable()
export class AdminService {

  constructor(private http: HttpClient, private errors: ApiErrorHandler) {
  }

  getAllUsers(): Observable<MaintainableUser[]> {
    return this.http.get<MaintainableUser[]>('api/admin/users').pipe(catchError(this.errors.handle));
  }

}

That's nice and clean and now I can use my service like this:

import {Component, OnInit} from '@angular/core';
import {MaintainableUser} from './maintainable-user';
import {AdminService} from '../admin.service';

@Component({
  selector: 'app-maintain-users',
  templateUrl: './maintain-users.component.html',
  styleUrls: ['./maintain-users.component.scss']
})
export class MaintainUsersComponent implements OnInit {

  constructor(private adminService: AdminService) {
  }

  ngOnInit() {
    this.reloadMaintainableUsers();
  }

  private reloadMaintainableUsers() {
    this.adminService.getAllMaintainableUsers().subscribe(
      data => {
        console.log(data);
        console.log(data[0] instanceof MaintainableUser);
        data[0].hasPrivilege('ADMIN');
      },
      err => {
        console.log('Service error: ' + err);
      }
    );
  }

}

Problem is that the data I receive is not actually a list of MaintainableUser. The response block from my MaintainableUsersComponent has the following output:

{"id": "john", "name": "John Doe", "privileges": ["ADMIN"]}

false

ERROR TypeError: _v.context.$implicit.hasPrivilege is not a function

Why is that? Why is my data[0] not instanceof MaintainableUser? I'm very new with Angular but I understand from different tutorials that since version 4 I should be able to auto map exactly like that to my classes/interfaces. Or is it just a fake thing so you could be safe that your object is strongly typed but you can not be sure it is instance of the actual object itself? It would be really nice if I could have some helper methods in my response classes so I could use them in view's for example, but currently I have not found a clean way to do it.

Upvotes: 2

Views: 1811

Answers (3)

krystianpe
krystianpe

Reputation: 136

It is rather problem with understanding how classes/objects work. What is returned from API is just a plain object (parsed JSON) which doesn't have any methods of MaintainableUser because it is not instance of your class. It only has same properties as your class.

You need to manually pass values from object into constructor and create instance of your class. Angular doesn't do it automatically. I often do something like this:

export class MaintainableUser {

  public static createInstance({ id, name, privileges }): MaintainableUser {
    return new MaintainableUser(id, name, privileges);
  }

  constructor(public id: string, public name: string, public privileges: string[]) {}

  public hasPrivilege(privilege: string): boolean {
    return this.privileges.indexOf(privilege) != -1;
  }

  public togglePrivilege(privilege: string) {
    if (this.hasPrivilege(privilege)) {
      this.privileges.splice(this.privileges.indexOf(privilege), 1);
    } else {
      this.privileges.push(privilege);
    }
    console.log(this.privileges);
  }
}

And then:

getAllUsers(): Observable<MaintainableUser[]> {
  return this.http.get<MaintainableUser[]>.('api/admin/users').pipe(
    map(MaintainableUser.createInstance)
    catchError(this.errors.handle)
 );
}

Upvotes: 3

joh04667
joh04667

Reputation: 7427

Your MaintainableUser type is being mapped just fine, so the HttpResponse is correctly mapping that generic type and the Typescript compiler isn't complaining, but your response is not an instance of MaintainableUser. It's still just an object that was parsed from JSON.

Your MaintainableUser class has a method property, which is equivalent to adding that method to MaintainableUser.prototype. In fact, that's exactly what the Typescript compiler does, when targeted to ES5.

Remember that Typescript types have no effect on the transpiled Javascript. Passing that MaintainableUser[] type to the HttpClient simply adds strong typing to the response. Essentially, you're just telling your IDE and the compiler that the Response is a MaintainableUser[], but it's not actually an instance of MaintainableUser. You need to use the new keyword to pass on that prototype.

I'd refactor MaintainableUser to accept that Response as a constructor parameter and map the response to a new instance of MaintainableUser, or move the hasPrivelage method to a service.

Upvotes: 1

JusMalcolm
JusMalcolm

Reputation: 1431

instanceof checks the prototype chain, so unless your getAllMaintainableUsers() is creating new MaintainableUser objects, it will return false. My guess (from your getAllUsers() call above) is that you are directly returning an object from a service. Even though you are annotating the type as MaintainableUser[], since it is not created with the prototype of MaintainableUser, instanceof will not return true for data[0] instanceof MaintainableUser.

Upvotes: 0

Related Questions