I am currently really confused, because I get the ERROR TypeError: "_this.device.addKeysToObj is not a function"
. But I implemented the function, so I have no idea what's the problem or why it is not callable. I have tried the code with Firefox and chrome, both through the same error.
The error is in line this.device.addKeysToObj(this.result.results[0]);
Here is my class:
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); } }
And that is the call:
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
Logging this.device
see comment in code above:
{ deviceID: "000000001" deviceType: "sensor" id: 5 location: "-" name: "Batteries" subType: "sensor" valueNamingMap: Object { v0: "vehicle battery", v1: "Living area battery" } <prototype>: Object { … } }
Edit 2
Part of the device.service code:
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>())) ); }
The call in the parent component:
ngOnInit() { this.deviceService.list('', 'sensor', ).subscribe( res => { this.devices = res.results; } ) }
Template:
<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>
This is a standard JavaScript error when trying to call a function before it is defined. This error occurs if you try to execute a function that is not initialized or is not initialized correctly. This means that the expression did not return a function object.
The TypeError object represents an error when an operation could not be performed, typically (but not exclusively) when a value is not of the expected type.
In short, the "is not a function" error means that the value you're trying to invoke is not a function. The most common reasons this error occurs are: defining a variable with the same name that shadows the original object. calling a method on a value of a different type, e.g. calling the Array.
This is a common gotcha with Typescript, you say device
is of type Device
, but it isn't. It has all of the same properties as a Device
would, but since it isn't a Device
it does not have the expected methods.
You need to ensure that you instantiate Device
for each entry in your Page
, perhaps in the ngOnInit
of the parent component:
I don't know the structure of Page
, but if it's an array try the following.
ngOnInit() { this.deviceService.list('', 'sensor', ).subscribe( res => { this.devices = res.results.map(x => Object.assign(new Device(), x)); } ) }
Let's try a typescript example, as this behaviour doesn't have anything to do with Angular. We'll use localStorage
to represent data coming from an external source, but this works just the same with 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);
That works just fine, awesome. A typescript interface is purely a compile-time structure and, unlike a class, it has no equivalent in JavaScript - it's just a developer hint. But this also means that if you create an interface for an external value, like SimpleValue
above, and get it wrong then the compiler is still going to trust you know what you're talking about, it can't possibly validate this at compile time.
What about loading a class from an external source? How does it differ? If we take the example above and change SimpleValue
into a class without changing anything else then it will still work. But there is a difference. Unlike interfaces, classes are transpiled into their JavaScript equivalent, in other words, they exist past the point of compilation. In our above example this doesn't cause a problem, so let's try an example that does cause a problem.
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
The loaded value had the properties we expected, but not the methods, uh oh! The problem is that methods get created when new SimpleClass
is called. When we created valueToSave
we did indeed instantiate the class, but then we turned it into a JSON string and sent it off elsewhere, and JSON has no concept of methods so the information was lost. When we loaded the data in loadFromStorage
we did not call new SimpleClass
, we just trusted that the caller knew what the stored type would be.
How do we deal with this? Let's go back to Angular for a moment and consider a common use case: dates. JSON has no Date type, JavaScript does, so how do we retrieve a date from our server and have it work as a date? Here's a pattern I like to use.
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 }); } }
Perhaps a bit verbose, but it's the most robust and flexible pattern I've used for dealing with rich frontend models coming from flat backend contracts.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With