typescript2.0 - Typescript med Angular HttpClient ErrorType ... är inte en funktion när man kallar klassmetod


Jag står inför ett konstigt (för mig) problem med objekt och typer i TypeScript/Angular.

Jag har en klass som beskriver mina objekt som mottagits från en fjärr-API:

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

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

Jag använder HttpClient för att hämta objekt från 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
        }
    );
}

Mitt problem är att när jag försöker kalla på metoden doSomething på en av mina Point från arrayen

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

Jag får ett konstigt fel på konsolen:

ERROR TypeError: firstPoint.doSomething is not a function

Jag har inte använt Typescript och Angular så länge, så jag antar att det är något fel på min kod, men jag kunde inte hitta något svar på mitt problem. Kan du ge mig några råd?

Svar #1

Problemet är att du faktiskt inte får instanser av Point från webbservicen. Du får ett JSON-objekt som har klassens fält, men inte metoderna. Du kan instansiera klassen och använda Object.assign för att tilldela värdena från objektliteralet till varje Point-instans

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

Svar #2

Object.assign() kommer att lösa ditt nuvarande problem, men inte om du har nästlade objekt. Då måste du göra Object.assign() för varje nästlat objekt också, vilket kan bli tråkigt om du måste göra detta på flera ställen i din kodbas.

Jag föreslår ett alternativ: class-transformer. Med detta kan du markera dina inbäddade fält med annotationer som berättar för kompilatorn hur de inbäddade objekten också ska skapas. Med detta behöver du bara använda plainToClass()-metoden för att mappa ditt objekt på högsta nivå och alla underliggande fält kommer också att ha korrekta typer/objekt.

Exempel

Låt oss säga att vi har två klasser:

class Parent {
    name: string;
    child: Child;

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

class Child{
    name: string;

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

Det första fallet vet vi redan inte fungerar:

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

Andra fallet med 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!

för att få det att fungera, skulle vi behöva göra följande:

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

Tredje fallet med class-transformer:

Först ändra föräldraklassen så att barnmappningen är definierad:

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

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

då kan du kartlägga till föräldraobjektet:

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

Relaterade artiklar