ERROR TypeError: Cannot read property 'toLowerCase' of null

Hello! I’m trying create a search bar from my json response. Well, with my keys of my json works, but with a specific key: publisher. return this error:

core.js:6157 ERROR TypeError: Cannot read property ‘toLowerCase’ of null
at filtro.pipe.ts:17
at Array.filter ()
at FiltroPipe.transform (filtro.pipe.ts:15)
at pureFunction2Internal (core.js:25605)
at ɵɵpipeBind2 (core.js:25779)
at AllPage_ion_list_7_Template (template.html:17)
at executeTemplate (core.js:9545)
at refreshView (core.js:9414)
at refreshEmbeddedViews (core.js:10534)
at refreshView (core.js:9438)

My html’s code:

<ion-header no-border>
  <ion-toolbar color="primary">
    <ion-buttons>
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
    <div class="search">
    <ion-searchbar id="custom-search" placeholder="Procurar personagem" animated (ionChange)="buscarHero($event)"></ion-searchbar>
    </div>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list *ngIf="heros.length > 0">
  <!--class="results" *ngFor="let hero of heros | filtro: busca" [routerLink]="'/hero/' + [hero.name]" [detail]="false" lines="none">-->
    <ion-grid>
      <ion-row>
        <ion-col size="4" *ngFor="let hero of heros | filtro: busca" [routerLink]="'/hero/' + [hero.name]">
          <p>{{hero.biography.publisher}}</p>
          <img src="{{hero.images.md}}">
        </ion-col>
      </ion-row>
    </ion-grid>
  </ion-list>

</ion-content>

My ts’s code:

import { Component, OnInit } from '@angular/core';
import { Api } from '../models/heros.model';
import { DataService} from '../Services/data.service';

@Component({
  selector: 'app-all',
  templateUrl: './all.page.html',
  styleUrls: ['./all.page.scss'],
})
export class AllPage implements OnInit {
  heros: Api[] = []
  busca = '';

  constructor(
    private data: DataService
  ) { 
    this.data.getHeros().subscribe(response => this.heros = response)
  }

  ngOnInit() {
  }

  buscarHero(event){
    const text = event.target.value;
    console.log(text)
    this.busca = text;
  }

}

My Pipe’s code

import { Pipe, PipeTransform } from '@angular/core';
import { Api } from '../models/heros.model';

@Pipe({
  name: 'filtro'
})

export class FiltroPipe implements PipeTransform {

  transform(heros: Api[], texto: string): Api[] {
    if (texto.length === 0){ return heros;}
    
    texto = texto.toLowerCase();
    var number: number;
    return heros.filter(hero =>{
      return hero.name.toLowerCase().includes(texto)
            || hero.biography.publisher.toLowerCase().includes(texto);
    })

  }


}

My json response (i will post just one, because is so long)

[
  {
    "id": 1,
    "name": "A-Bomb",
    "slug": "1-a-bomb",
    "powerstats": {
      "intelligence": 38,
      "strength": 100,
      "speed": 17,
      "durability": 80,
      "power": 24,
      "combat": 64
    },
    "appearance": {
      "gender": "Male",
      "race": "Human",
      "height": [
        "6'8",
        "203 cm"
      ],
      "weight": [
        "980 lb",
        "441 kg"
      ],
      "eyeColor": "Yellow",
      "hairColor": "No Hair"
    },
    "biography": {
      "fullName": "Richard Milhouse Jones",
      "alterEgos": "No alter egos found.",
      "aliases": [
        "Rick Jones"
      ],
      "placeOfBirth": "Scarsdale, Arizona",
      "firstAppearance": "Hulk Vol 2 #2 (April, 2008) (as A-Bomb)",
      "publisher": "Marvel Comics",
      "alignment": "good"
    },
    "work": {
      "occupation": "Musician, adventurer, author; formerly talk show host",
      "base": "-"
    },
    "connections": {
      "groupAffiliation": "Hulk Family; Excelsior (sponsor), Avengers (honorary member); formerly partner of the Hulk, Captain America and Captain Marvel; Teen Brigade; ally of Rom",
      "relatives": "Marlo Chandler-Jones (wife); Polly (aunt); Mrs. Chandler (mother-in-law); Keith Chandler, Ray Chandler, three unidentified others (brothers-in-law); unidentified father (deceased); Jackie Shorr (alleged mother; unconfirmed)"
    },
    "images": {
      "xs": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/xs/1-a-bomb.jpg",
      "sm": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/sm/1-a-bomb.jpg",
      "md": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/md/1-a-bomb.jpg",
      "lg": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/lg/1-a-bomb.jpg"
    }
  }
]

So, anybody can helps me?

Do a console log on hero in the pipe before the filter. And check the entry that shows before the error

You may find an error in your json this way, the necessarity to recode snd/or build safeguards on some properties

2 Likes

If you want practical advice, read @Tommertom’s post above mine.

If you want some opinionated theory, I believe in contracts as a core concept of programming. You don’t show the definition of Api (and, incidentally, why are you calling what appears to be a Hero an Api?), so I’m going to assume it looks something like this:

interface Biography {
  publisher: string;
}

interface Hero {
  name: string;
  biography: Biography;
} 

These interfaces represent promises that any part of your code that spawns Heros must obey. A Hero must have a name, it must have a biography, and that biography must have a name.

Maybe those guarantees are too strong. Perhaps some heroes don’t have biographies. If so, be honest about that:

interface Hero {
  name: string;
  biography?: Biography;
}

This makes explicit the fact that biography might be undefined, and clients are now responsible for checking that. Your FiltroPipe code will now generate a build-time error, which is a huge improvement over the runtime error you’re seeing now.

One convenient thing about using interfaces for data that comes over the wire from backend services is that there isn’t any need for boilerplate “blessing” code. However, when you do something like this:

class HeroService {
  fetchAllHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>("/api/heroes");
  }
}

…now you’ve expanded the contract to cover the things that the API sends you. Now the backend is responsible for ensuring every Hero has a biography.

It is often the case that the guarantees that make sense for the world inside the client app are not identical to those that make sense for the backend. Maybe some heroes don’t have biographies, but in your app you want to paper over that.

It is essential to do that papering in the first point of contact. Fill biography in inside fetchAllHeroes if you can’t trust that the API always does it.

Bottom line: the more honest, detailed, and accurate your typing is, the better your build tools can help you find bugs like this before runtime.

2 Likes

Thanks for your reply. But this error was in the json response. Some publisher heros was iguals null, so i changed and works fine. :smiley:

And i get Hero Api from my service and my interface. I will post both for question of curiosity:

Api Interface:

    export interface Api{
    id: number;
    name: string;
    slug: string;
    powerstats: Powerstats;
    appearance: Appearance;
    biography: Biography;
    work: Work;
    connections: Connections;
    images: Images;
  
  }
    
    interface Powerstats {
    intelligence: number;
    strength: number;
    speed: number;
    durability: number;
    power: number;
    combat: number;
  }
  
    interface Appearance {
    gender: string;
    race: string;
    height: string[];
    weight: string[];
    eyeColor: string;
    hairColor: string;
  }
  
    interface Biography {
    fullName: string;
    alterEgos: string;
    aliases: string[];
    placeOfBirth: string;
    firstAppearance: string;
    editora: string;
    alignment: string;
  }
  
    interface Work {
    occupation: string;
    base: string;
  }
  
    interface Connections {
    groupAffiliation: string;
    relatives: string;
  }
  
    interface Images {
    xs: string;
    sm: string;
    md: string;
    lg: string;
  }

Data Service

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Api } from '../models/heros.model';
import { Editoras } from '../models/editoras.model';

@Injectable({
  providedIn: 'root'
})
export class DataService {

  constructor(private http: HttpClient) { }

  getHeros(){
    return this.http.get<Api[]>('https://editoras-api.herokuapp.com/api/heros');
  }

  getEditoras(){
    return this.http.get<Editoras[]>('http://editoras-api.herokuapp.com/api/editoras');
  }
}

In your opnion this was a good practice? A clean code?

Thanks

The primary answer to that depends on your response to this:

Which more accurately represents what you mean by “I changed” there?

A. I changed how the backend responds so that the client can safely rely on every Hero having a publisher.
B. I threw a bunch of line noise like hero?.publisher?.name in the template.

If (A), then yes, I think what you posted here is generally fine, except I still hate Api as a name, because each instance of that interface is not an API; it’s a Hero. I also strongly dislike providedIn: 'root' because it makes it impossible to mock out DataService for testing.

If what you instead meant by “I changed” to mean that you did something else in your client-side code to deal with the fact that some heroes do not have biographies, then no, if I were in your position, I would do one of two things: either weaken the type of biography to be Biography | undefined, or assign some sort of dummy object to the biography of every Hero inside the getHeros method.

1 Like

The json response is from my api, so i changed inside my json response. I didnt make changes from my code in Ionic. More one thing, my english isn’t good, but i hope you got it understand my situation and my solve.

I think what you are calling “my json response”, I’m calling “your backend”, so yes, you fixed it outside the Ionic world, and the Ionic code can safely rely on every Hero having a biography. I agree that that’s probably the best way to address the issue.

2 Likes

Thanks for your support. :smiley:

1 Like