web-dev-qa-db-fra.com

Angular 6: ERREUR TypeError: "... n'est pas une fonction" - mais c'est

Je suis actuellement très confus, car je reçois le ERROR TypeError: "_this.device.addKeysToObj is not a function". Mais j'ai implémenté la fonction, je n'ai donc aucune idée du problème ni de la raison pour laquelle elle n'est pas appelable. J'ai essayé le code avec Firefox et chrome, tous deux par la même erreur.

L'erreur est dans la ligne this.device.addKeysToObj(this.result.results[0]);

Voici ma classe:

export class Device {
    id: number;
    deviceID: string;
    name: string;
    location: string;
    deviceType: string;
    subType: string;
    valueNamingMap: Object;

    addKeysToObj(deviceValues: object): void {
        for (let key of Object.keys(deviceValues).map((key) => { return key })) {
            if (!this.valueNamingMap.hasOwnProperty(key)) {
                this.valueNamingMap[key] = '';
            }
        }
        console.log(this, deviceValues);
    }
}

Et c'est l'appel:

export class BatterieSensorComponent implements OnInit {
    @Input() device: Device;
    public result: Page<Value> = new Page<Value>();

    //[..]

    ngOnInit() {
      this.valueService.list('', this.device).subscribe(
        res => {
          console.log(this.device);  // NEW edit 1
          this.result = res;
          if (this.result.count > 0) 
          {
            this.device.addKeysToObj(this.result.results[0]);
          }
        }
      )
    }
}

Edit 1

Journalisation this.device Voir le commentaire dans le code ci-dessus:

{
    deviceID: "000000001" 
    deviceType: "sensor"    ​
    id: 5    ​
    location: "-"
​    name: "Batteries"    ​
    subType: "sensor"    ​
    valueNamingMap:
      Object { v0: "vehicle battery", v1: "Living area battery" }
    <prototype>: Object { … } 
}

Edit 2

Une partie du code device.service:

list(url?: string, deviceType?: string, subType?: string): Observable<Page<Device>> {
  if(!url) url = `${this.url}/devices/`;
  if(deviceType) url+= '?deviceType=' + deviceType;
  if(subType) url+= '&subType=' + subType;

  return this.httpClient.get<Page<Device>>(url, { headers: this.headers })
    .pipe(
      catchError(this.handleError('LIST devices', new Page<Device>()))
    );
}

L'appel dans le composant parent:

ngOnInit() {
  this.deviceService.list('', 'sensor', ).subscribe(
    res => { 
      this.devices = res.results;
    }
  )
}

Modèle:

<div class="mdl-grid">
  <div class="mdl-cell mdl-cell--6-col mdl-cell--6-col-tablet" *ngFor="let device of devices">
    <app-batterie-sensor [device]="device"></app-batterie-sensor>
  </div>
</div>
17
Max

Réponse originale

Ceci est un casse-tête commun avec TypeScript, vous dites que device est de type Device, mais ce n'est pas le cas. Il a toutes les mêmes propriétés qu'un Device, mais comme ce n'est pas réellement un Device, il n'a pas les méthodes attendues.

Vous devez vous assurer que vous instanciez Device pour chaque entrée de votre Page, peut-être dans le ngOnInit du composant parent:

Je ne connais pas la structure de Page, mais si c'est un tableau, essayez ce qui suit.

ngOnInit() {
  this.deviceService.list('', 'sensor', ).subscribe(
    res => { 
      this.devices = res.results.map(x => Object.assign(new Device(), x));
    }
  )
}

Plus d'explications

Essayons un exemple avec TypeScript, car ce comportement n’a vraiment rien à voir avec Angular. Nous utiliserons localStorage pour représenter les données provenant d'une source externe, mais cela fonctionne de la même manière avec HTTP.

interface SimpleValue {
    a: number;
    b: string;
}

function loadFromStorage<T>(): T {
    // Get from local storage.
    // Ignore the potential null value because we know this key will exist.
    const storedValue = localStorage.getItem('MyKey') as string;

    // Note how there is no validation in this function.
    // I can't validate that the loaded value is actually T
    // because I don't know what T is.
    return JSON.parse(storedValue);
}

const valueToSave: SimpleValue = { a: 1, b: 'b' };
localStorage.setItem('MyKey', JSON.stringify(valueToSave));

const loadedValue = loadFromStorage<SimpleValue>();

// It works!
console.log(loadedValue);

Cela fonctionne très bien, génial. Une interface TypeScript est purement une structure de temps de compilation. Contrairement à une classe, elle n’a pas d’équivalent en JavaScript, c’est juste un indice pour le développeur. Mais cela signifie également que si vous créez une interface pour une valeur externe, telle que SimpleValue ci-dessus, et que vous l'obtenez faux, le compilateur continuera à vous faire confiance et saura que vous savez de quoi vous parlez. à propos, il ne peut pas éventuellement valider cela au moment de la compilation.

Qu'en est-il du chargement d'une classe à partir d'une source externe? Comment est-ce différent? Si nous prenons l'exemple ci-dessus et changeons SimpleValue en classe sans rien changer d'autre, cela fonctionnera toujours. Mais il y a une différence. Contrairement aux interfaces, les classes sont en fait transpilées dans leur équivalent JavaScript. En d'autres termes, elles existent réellement après la compilation. Dans l'exemple ci-dessus, cela ne pose pas de problème. Essayons donc un exemple qui pose problème.

class SimpleClass {
    constructor(public a: number, public b: string) { }

    printA() {
        console.log(this.a);
    }
}

const valueToSave: SimpleClass = new SimpleClass(1, 'b');
localStorage.setItem('MyKey', JSON.stringify(valueToSave));

const loadedValue = loadFromStorage<SimpleClass>();

console.log(loadedValue.a); // 1
console.log(loadedValue.b); // 'b'
loadedValue.printA(); // TypeError: loadedValue.printA is not a function

La valeur chargée avait les propriétés que nous attendions, mais pas les méthodes, euh oh! Le problème est que les méthodes sont créées lorsque new SimpleClass est appelé. Lorsque nous avons créé valueToSave, nous avons effectivement instancié la classe, mais nous l'avons ensuite transformée en chaîne JSON et l'avons envoyée ailleurs, et JSON n'a pas de concept de méthode et l'information a donc été perdue. Lorsque nous avons chargé les données dans loadFromStorage, nous avons fait pas appeler new SimpleClass, nous venons de faire confiance à l’appelant qui sait quel type de type sera stocké.

Comment traitons-nous cela? Revenons un instant à Angular et considérons un cas d'utilisation courant: les dates. JSON n'a pas de type de date, mais JavaScript, comment pouvons-nous récupérer une date sur notre serveur et la faire fonctionner comme une date? Voici un modèle que j'aime utiliser.

interface UserContract {
    id: string;
    name: string;
    lastLogin: string; // ISO string representation of a Date.
}

class UserModel {
    id: string; // Same as above
    name: string; // Same as above
    lastLogin: Date; // Different!

    constructor(contract: UserContract) {
        // This is the explicit version of the constructor.
        this.id = contract.id;
        this.name = contract.name;
        this.lastLogin = new Date(contract.lastLogin);

        // If you want to avoid the boilerplate (and safety) of the explicit constructor
        // an alternative is to use Object.assign:
        // Object.assign(this, contract, { lastLogin: new Date(contract.lastLogin) });
    }

    printFriendlyLastLogin() {
        console.log(this.lastLogin.toLocaleString());
    }
}

import { HttpClient } from '@angular/common/http';
import { Injectable, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
class MyService {
    constructor(private httpClient: HttpClient) { }

    getUser(): Observable<UserModel> {
        // Contract represents the data being returned from the external data source.
        this.httpClient.get<UserContract>('my.totally.not.real.api.com')
            .subscribe(contract => new UserModel(contract));
    }
}

@Component({
    // bla bla
})
class MyComponent implements OnInit {
    constructor(private myService: MyService) { }

    ngOnInit() {
        this.myService.getUser().subscribe(x => {
            x.printFriendlyLastLogin(); // this works
            console.log(x.lastLogin.getFullYear()); // this works too
        });
    }
}

Peut-être un peu prolixe, mais c’est le modèle le plus robuste et le plus flexible que j’ai utilisé pour traiter avec des modèles frontaux riches issus de contrats d’arrière plan plats.

28
UncleDave