You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 17 Pokedex详情页控制台报错问题求助

问题:Angular Pokedex详情页控制台报错,部分宝可梦无法正常显示

进入单只宝可梦详情页时,页面能渲染数据,但控制台抛出以下错误:

detail.component.ts:79 ERROR TypeError: Cannot read properties of undefined (reading 'type')
    at DetailComponent_Template (detail.component.ts:26:12)
    at executeTemplate (core.mjs:11223:9)
    at refreshView (core.mjs:12746:13)
    at detectChangesInView$1 (core.mjs:12970:9)
    at detectChangesInViewIfAttached (core.mjs:12933:5)
    at detectChangesInComponent (core.mjs:12922:5)
    at detectChangesInChildComponents (core.mjs:12983:9)
    at refreshView (core.mjs:12796:13)
    at detectChangesInView$1 (core.mjs:12970:9)
    at detectChangesInViewIfAttached (core.mjs:12933:5)

部分宝可梦无法正常显示。尝试使用可选链操作符:

<h2>{{ this.pokemonDetails.name?.toUpperCase() }}</h2>

但出现橙色警告。相关代码如下:

组件代码(DetailComponent)

@Component({
  selector: 'app-detail',
  standalone: true,
  imports: [DetailComponent],
  template: `
    <div>
      <h2>{{ this.pokemonDetails.name.toUpperCase() }}</h2>
      <img
        src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/{{
          pokemonId
        }}.png"
        alt="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/{{
          pokemonId
        }}.png"
        height="100"
      />
      <div>
        <p>Pokemon type:</p>
        <p>{{ this.pokemonDetails.types[0].type.name }}</p>
        <p>{{ this.pokemonDetails.types[1].type.name }}</p>
      </div>
      <div>
        <p>
          Description:
          {{ this.speciesDetail.flavor_text_entries[0].flavor_text }}
        </p>
      </div>
      <div>
        <p>BASE STAT:</p>
        <ul>
          <li>
            <p>HP:{{ this.pokemonDetails.stats[0].base_stat }}</p>
          </li>
          <li>
            <p>ATK:{{ this.pokemonDetails.stats[1].base_stat }}</p>
          </li>
          <li>
            <p>DEF:{{ this.pokemonDetails.stats[2].base_stat }}</p>
          </li>
          <li>
            <p>S. ATK:{{ this.pokemonDetails.stats[3].base_stat }}</p>
          </li>
          <li>
            <p>S. DEF:{{ this.pokemonDetails.stats[4].base_stat }}</p>
          </li>
          <li>
            <p>SPEED:{{ this.pokemonDetails.stats[5].base_stat }}</p>
          </li>
        </ul>
      </div>
    </div>
  `,
  styles: ``,
})
export default class DetailComponent implements OnInit {
  pokemonId!: number;
  pokemonDetails!: Pokemon;
  speciesDetail!: Species;

  public service = inject(StateService);

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    this.route.params.subscribe((params) => {
      this.pokemonId = params['id'];
      this.service.getPokemonById(this.pokemonId).subscribe((pokemon) => {
        this.pokemonDetails = pokemon;
        console.log(this.pokemonDetails);
      });
      this.service
        .getPokemonSpeciesDetailsById(this.pokemonId)
        .subscribe((pokemon) => {
          this.speciesDetail = pokemon;
          console.log(this.speciesDetail);
        });
    });
  }
}

服务代码(StateService)

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, forkJoin } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { Pokemon, PokemonResults } from '../../model/pokemon';

@Injectable({
  providedIn: 'root',
})
export class StateService {
  private pokemonListSubject: BehaviorSubject<Pokemon[]> = new BehaviorSubject<
    Pokemon[]
  >([]);
  public pokemonList$: Observable<Pokemon[]> =
    this.pokemonListSubject.asObservable();

  constructor(private http: HttpClient) {}

  fetchPokemonList(offset: number = 0, limit: number = 20): Observable<void> {
    const url = `https://pokeapi.co/api/v2/pokemon/?offset=${offset}&limit=${limit}`;
    return this.http.get<PokemonResults>(url).pipe(
      map((data: PokemonResults) => data.results),
      mergeMap((pokemonResults) =>
        this.fetchDetailedPokemonData(pokemonResults)
      ),
      map((detailedPokemonList) => {
        this.pokemonListSubject.next(detailedPokemonList);
      })
    );
  }

  private fetchDetailedPokemonData(
    pokemonList: { name: string; url: string }[]
  ): Observable<Pokemon[]> {
    return forkJoin(
      pokemonList.map((pokemon) => this.http.get<Pokemon>(pokemon.url))
    );
  }
  getPokemonById(id: number): Observable<Pokemon> {
    const url = `https://pokeapi.co/api/v2/pokemon/${id}`;
    return this.http.get<Pokemon>(url);
  }

  getPokemonSpeciesDetailsById(id: number): Observable<any> {
    const url = `https://pokeapi.co/api/v2/pokemon-species/${id}`;
    return this.http.get<any>(url);
  }
}

解决方案

1. 核心问题分析

报错原因是数据异步加载导致模板渲染时,pokemonDetails或speciesDetail还未初始化,直接访问其深层属性(如types[0].type.name)会触发undefined读取错误。另外,部分宝可梦可能只有1种属性,直接访问types[1]也会报错。

2. 具体修复步骤

(1)初始化对象,避免undefined

组件中声明的pokemonDetails和speciesDetail用了非空断言!,但初始状态仍为undefined,建议初始化为空对象或对应类型的默认值:

pokemonDetails: Pokemon = {} as Pokemon;
speciesDetail: Species = {} as Species;

(2)在模板中正确使用可选链和空值合并操作符

  • 移除模板中的this.,Angular模板中直接访问组件属性即可,不需要this,这也是使用可选链出现橙色警告的原因。
  • 对所有深层属性使用可选链?.,防止访问undefined属性;对可能不存在的项(如第二个属性),用空值合并??提供默认值。

修改后的模板部分:

<div>
  <h2>{{ pokemonDetails.name?.toUpperCase() ?? 'Loading...' }}</h2>
  <img
    src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home/{{ pokemonId }}.png"
    alt="Pokemon sprite"
    height="100"
  />
  <div>
    <p>Pokemon type:</p>
    <p>{{ pokemonDetails.types?.[0]?.type?.name ?? 'N/A' }}</p>
    <p>{{ pokemonDetails.types?.[1]?.type?.name ?? 'N/A' }}</p>
  </div>
  <div>
    <p>
      Description:
      {{ speciesDetail.flavor_text_entries?.[0]?.flavor_text ?? 'No description available' }}
    </p>
  </div>
  <div>
    <p>BASE STAT:</p>
    <ul>
      <li><p>HP:{{ pokemonDetails.stats?.[0]?.base_stat ?? 0 }}</p></li>
      <li><p>ATK:{{ pokemonDetails.stats?.[1]?.base_stat ?? 0 }}</p></li>
      <li><p>DEF:{{ pokemonDetails.stats?.[2]?.base_stat ?? 0 }}</p></li>
      <li><p>S. ATK:{{ pokemonDetails.stats?.[3]?.base_stat ?? 0 }}</p></li>
      <li><p>S. DEF:{{ pokemonDetails.stats?.[4]?.base_stat ?? 0 }}</p></li>
      <li><p>SPEED:{{ pokemonDetails.stats?.[5]?.base_stat ?? 0 }}</p></li>
    </ul>
  </div>
</div>

(3)优化异步数据加载逻辑

用forkJoin合并两个API请求,确保数据都加载完成后再更新视图,避免部分数据先到导致的渲染问题:

ngOnInit(): void {
  this.route.params.subscribe((params) => {
    this.pokemonId = params['id'];
    forkJoin([
      this.service.getPokemonById(this.pokemonId),
      this.service.getPokemonSpeciesDetailsById(this.pokemonId)
    ]).subscribe(([pokemon, species]) => {
      this.pokemonDetails = pokemon;
      this.speciesDetail = species;
    });
  });
}

(4)添加加载状态提示

在模板顶部添加加载状态,当数据未加载完成时显示:

<div *ngIf="!pokemonDetails.name && !speciesDetail.flavor_text_entries">
  Loading Pokemon details...
</div>
<div *ngIf="pokemonDetails.name || speciesDetail.flavor_text_entries">
  <!-- 原有详情内容 -->
</div>

3. 额外注意事项

  • Angular模板中不需要使用this.访问组件属性,这是TypeScript代码中的写法,模板直接写属性名即可,否则会触发警告。
  • 非空断言!仅告诉TypeScript变量会被初始化,但运行时如果未初始化仍会报错,所以初始化空对象更可靠。
  • 部分宝可梦(如百变怪)只有1种属性,types[1]不存在,必须用可选链或条件判断处理。

内容的提问来源于stack exchange,提问作者Daniele Quintiliani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 16:05:56