Search code examples
angulartypescriptangular5angular-httpclient

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.


Solution

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