user3626048
user3626048

Reputation: 736

Typescript with Angular HttpClient ErrorType ... is not a function when calling class method

I'm facing some weird (to me) problem with objects and types in typescript/angular.

I have some class describing my objects received from remote API:

export class Point {
    lat:number;
    lng:number;
    name:string;

    public doSomething() {
        console.log("doSomething called");
    }
}

I'm using HttpClient to get objects from API:

constructor(private http:HttpClient) {}

getPointsAsync(callback: (points: Point[]) => {}) {
    this.http.get<Point[]>(`${environment.apiUrl}/api/points`)
    .subscribe(
        (result: Point[]) => {
            //do something with data
            callback(result);
        },
        error => { //some error handling
        }
    );
}

My problem is that when I try to call method doSomething on one of my Point from array

var firstPoint = points[0];
firstPoint.doSomething()

I get some weird error on console:

ERROR TypeError: firstPoint.doSomething is not a function

I'm not using Typescript and Angular for very long so I'm assuming that it is something wrong with my code but I couldn't find answer to my issue. Could you give me some advice?

Upvotes: 3

Views: 642

Answers (2)

Babyburger
Babyburger

Reputation: 1830

Object.assign() will solve your current problem, but not if you have nested objects. Then you will have to do Object.assign() for each nested object as well, which can get tedious if you have to do this in multiple places in your codebase.

I suggest an alternative: class-transformer With this you can mark your nested fields with annotations that tell the compiler how to create the nested objects as well. With this you only need to use the plainToClass() method to map your top level object and all the underlying fields will also have the correct types/objects.

Example

Let's say we have two classes:

class Parent {
    name: string;
    child: Child;

    public getText(): string {
        return 'parent text';
    }
}

class Child{
    name: string;

    public getText(): string {
        return 'child text';
    }
}

The first case we already know doesn't work:

let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
let parent: Parent = parentJson; // note: compiler accepts this because parentJson is any.  
        // If we tried to assign the json structure directly to 'parent' it would fail because the compiler knows that the method getText() is missing!

console.log(parent.getText()); // throws the error that parent.getText() is not a function as expected

Second case using Object.assign():

let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
let parent: Parent = Object.assign(parentJson); 

console.log(parent.getText()); // this works
console.log(parent.child.getText()); // throws error that parent.child.getText() is not a function!

to make it work, we would have to do the following:

let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
let parent: Parent = Object.assign(parentJson);
parent.child = Object.assign(parentJson.child);

console.log(parent.getText()); // this works
console.log(parent.child.getText()); // this works

Third case with class-transformer:

First modify the parent class so that the child mapping is defined:

class Parent {
    name: string;
    @Type(() => Child)
    child: Child;

    public getText(): string {
        return 'parent text';
    }
}

then you can map to the parent object:

let parentJson: any = {name: 'parent name', child: {name: 'child name'}};
let parent: Parent = plainToClass(Parent, parentJson);

console.log(parent.getText()); // this works
console.log(parent.child.getText()); // this works

Upvotes: 1

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249506

The problem is that you don' actually get instances of Point from the web service. You get a JSON object that has the class fields, but not the methods. You can use instantiate the class and use Object.assign to assign the values from the object literal to each Point instance

getPointsAsync(callback: (points: Partial<Point>[]) => {}) {
    this.http.get<Point[]>(`${environment.apiUrl}/api/points`)
        .subscribe(
            (result: Partial<Point>[]) => {
                let points = result.map(p=> Object.assign(new Point(), p));
                callback(points);
            },
            error => { //some error handling
            }
        );
}

Upvotes: 3

Related Questions