Angular 6: ERROR TypeError: "... is not a function" - but it is
Asked Answered
S

5

53

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>
Seftton answered 9/8, 2018 at 9:44 Comment(0)
H
93

Original answer

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

Further explanation

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.

Hedvah answered 9/8, 2018 at 10:10 Comment(5)
Thanks for the explanation! Can you share a bit more on why does this happen? Why the objects are not the same?Hest
It's not a Device unless you explicitly instantiate it with new Device(), which was not being done in the original question.Hedvah
I was looking for some more details regarding this behaviour. The post Angular HTTP tips for success helped me a lot.Hest
Ha, came looking for the solution to the function thing, and immediately found a nice way to clone objects.Forespeak
This explains a lot of things! Great explanation.Weston
E
2

You might have landed here with a different problem than in the accepted answer: If you're using Angular's services and forget the @Injectable, with Angular Ivy you get a runtime exception like this:

ERROR TypeError: ConfigurationServiceImpl.\u0275fac is not a function

The correct solution is to ad @Injectable also to implementations, e.g:

// do not omit the @Injectable(), or you'll end up with the error message!
@Injectable()
export class ConfigurationServiceImpl implements ConfigurationService {
...
}

@Injectable({
  providedIn: "root",
  useClass: ConfigurationServiceImpl,
})
export abstract class ConfigurationService {
...
}

also see Angular 7 TypeError: service.x is not a function.

Emmerie answered 24/5, 2021 at 11:39 Comment(0)
C
0

In my case I tested two solutions that work for me

Wrap the code in a setTimeout

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

OR

The other solution was to add a condition

ngOnInit() {
  if(typeof this.deviceService.list === 'function'){ // START OF CONDITION
    this.deviceService.list('', 'sensor', ).subscribe(
      res => { 
        this.devices = res.results.map(x => Object.assign(new Device(), x));
      }
    )
  } // END OF CONDITION
}
Colombes answered 6/5, 2021 at 21:3 Comment(0)
C
0

As @UncleDave already explained, you're only mapping the values with corresponding names to the Typescript object but you're not creating the expected class object with it. It's pretty confusing, I know.

Object.assign() will solve your current problem, but not if you have nested objects. Then you will have to do Object.assign() for each nested object as well, which can get tedious if you have to do this in multiple places in your codebase.

I suggest an alternative: class-transformer With this you can mark your nested fields with annotations that tell the compiler how to create the nested objects as well. With this you only need to use the plainToClass() method to map your top level object and all the underlying fields will also have the correct types/objects.

Example

Let's say we have two classes:

class Parent {
    name: string;
    child: Child;

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

class Child{
    name: string;

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

The first case we already know doesn't work:

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

Second case using 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!

to make it work, we would have to do the following:

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

Third case with class-transformer:

First modify the parent class so that the child mapping is defined:

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

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

then you can map to the parent object:

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
Coquet answered 26/1, 2022 at 15:35 Comment(0)
U
-1

The answer I gave in this post helped me out. I got it from a YT video basically said you needed to check if that class's method exists. https://mcmap.net/q/262518/-method-in-typescript-class-give-error-quot-is-not-a-function-quot

Unit answered 6/5, 2023 at 1:43 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Potty

© 2022 - 2024 — McMap. All rights reserved.