Angular 6: ERROR TypeError: "... не является функцией", но это

Я в настоящее время очень смущен, потому что получаю ERROR TypeError: "_this.device.addKeysToObj is not a function". Но я реализовал функцию, поэтому я понятия не имею, в чем проблема или почему она не может быть вызвана. Я пробовал код с Firefox и хром, как через ту же ошибку.

Ошибка в строке this.device.addKeysToObj(this.result.results[0]);

Вот мой класс:

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

И это призыв:

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

Редактировать 1

Logging this.device см. Комментарий в коде выше:

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

Изменить 2

Часть кода 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>()))
    );
}

Вызов в родительском компоненте:

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

Шаблон:

<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>

Ответ 1

Оригинальный ответ

Это распространенная ошибка в Typescript, вы говорите, что device имеет тип Device, но это не так. Он имеет все те же свойства, что и Device, но, поскольку он не является Device, он не имеет ожидаемых методов.

Вам необходимо убедиться, что вы создаете экземпляр Device для каждой записи в вашем Page, возможно, в ngOnInit родительского компонента:

Я не знаю структуру Page, но если это массив, попробуйте следующее.

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

Дальнейшее объяснение

Давайте попробуем пример машинописного текста, так как это поведение не имеет ничего общего с Angular. Мы будем использовать localStorage для представления данных, поступающих из внешнего источника, но это работает точно так же с 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);

Это прекрасно работает, круто. Интерфейс машинописного текста - это просто структура времени компиляции, и, в отличие от класса, он не имеет эквивалента в JavaScript - это всего лишь подсказка разработчика. Но это также означает, что если вы создадите интерфейс для внешнего значения, как SimpleValue выше, и ошибетесь, то компилятор все еще будет доверять вам, что вы говорите, он не сможет проверить это время компиляции.

Как насчет загрузки класса из внешнего источника? Чем он отличается? Если мы возьмем приведенный выше пример и изменим SimpleValue на класс, ничего не меняя, он все равно будет работать. Но есть разница. В отличие от интерфейсов, классы преобразуются в их эквивалент JavaScript, другими словами, они существуют после точки компиляции. В нашем примере выше это не вызывает проблем, поэтому давайте попробуем пример, который вызывает проблемы.

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

Загруженное значение имело свойства, которые мы ожидали, но не методы, ах! Проблема в том, что методы создаются при вызове new SimpleClass. Когда мы создали valueToSave, мы действительно создали экземпляр класса, но затем мы превратили его в строку JSON и отослали его в другое место, и у JSON нет понятия методов, поэтому информация была потеряна. Когда мы загружали данные в loadFromStorage, мы не вызывали new SimpleClass, мы просто верили, что вызывающая сторона знает, каким будет сохраненный тип.

Как мы справимся с этим? Давайте на минуту вернемся к Angular и рассмотрим общий вариант использования: даты. В JSON нет типа Date, а в JavaScript - так, как мы можем получить дату с нашего сервера и заставить ее работать как дата? Вот шаблон, который мне нравится использовать.

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';
import { map } from 'rxjs/operators';

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

    getUser(): Observable<UserModel> {
        // Contract represents the data being returned from the external data source.
        return this.httpClient.get<UserContract>('my.totally.not.real.api.com')
            .pipe(
              map(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
        });
    }
}

Возможно, немного многословно, но это самый надежный и гибкий шаблон, который я использовал для работы с богатыми внешними моделями, основанными на контрактах с плоскими бэкэндами.