3

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?

2 Answers 2

3

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
            }
        );
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for your answer. Do I have to call Object.assign in every request where I need instances of specific class? Or maybe is it possible to move it elsewhere e.g. HttpInterceptor?
@user3626048 I am not aware of another way to do it, you would need to pass the constructor somehow (the type parameter is not enough as that get erased at runtime)
1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.