Ответ 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
});
}
}
Возможно, немного многословно, но это самый надежный и гибкий шаблон, который я использовал для работы с богатыми внешними моделями, основанными на контрактах с плоскими бэкэндами.