Troubleshooting beim Update von Angular 14 auf 15
18.01.2023, Michael Reimer

Ist das Angular Update nicht so gelaufen, wie erwartet? In diesem Blogpost zeigen wir, was beim Update von Angular 14 auf 15 bei uns schiefgelaufen ist und wie wir das Problem gelöst haben.

Ausgangslage

Auch schon einmal in ein ähnliches Problem gelaufen? Eigentlich ist nach der Vorbereitung laut offiziellem Update Manual alles wie erwartet verlaufen, der Befehl ng update @angular/core@latest @angular/cli@latest führt auch wie erwartet aus und dann passiert das:

Screenshot des Fehlers im CLI beim Angular Update

Fehlermeldung:

✖ Migration failed: Path “src/test.ts” does not exist.

Ein Blick in die package.json Datei hat keine offensichtlichen Fehler aufgezeigt. Alle Dependencies wurden wie erwartet installiert. Ein kurzer Check mit ng serve zeigt auch keine Auffälligkeiten - die Applikation kompiliert. Der Lauf des Scripts wurde jedoch unterbrochen, also dürfte es Stellen gegeben haben, an denen das Update nicht vollständig abgeschlossen wurde. Beim Testen der Applikation stellte sich Folgendes heraus:

Das Problem

Beim Aufruf von einigen Komponenten tauchte die folgende Fehlermeldung auf:

ERROR Error: Uncaught (in promise): TypeError: Cannot read properties of undefined
(reading 'getUserPreference')
TypeError: Cannot read properties of undefined (reading 'getUserPreference')
    at new ViewComponent (view.component.ts:410:31)
    at NodeInjectorFactory.ViewComponent_Factory [as factory] (view.component.ts:375:28)
    at getNodeInjectable (core.mjs:3485:44)
    at createRootComponent (core.mjs:12391:35)
    at ComponentFactory.create (core.mjs:12272:25)
    at ViewContainerRef.createComponent (core.mjs:21416:47)
    at RouterOutlet.activateWith (router.mjs:2611:39)
    at ActivateRoutes.activateRoutes (router.mjs:3023:40)
    at router.mjs:2972:18
    at Array.forEach (<anonymous>)
    at resolvePromise (zone.js:1214:31)
    at resolvePromise (zone.js:1168:17)
    at zone.js:1281:17
    at _ZoneDelegate.invokeTask (zone.js:409:31)
    at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:24002:28)
    at _ZoneDelegate.invokeTask (zone.js:408:60)
    at Object.onInvokeTask (core.mjs:24300:33)
    at _ZoneDelegate.invokeTask (zone.js:408:60)
    at Zone.runTask (zone.js:178:47)
    at drainMicroTaskQueue (zone.js:588:35)

Nach näherer Betrachtung der im StackTrace gelisteten ViewComponent und der Methode getUserPreference stellte sich heraus, dass Injected Services, welche direkt bei der Konstruktion einer Klasse von Class Fields aufgerufen wurden, diesen Fehler hervorgerufen haben.

export class ViewComponent extends AbstractComponentDirective implements OnInit {
  state: ViewState = {
    theme: this.themeService.getUserPreference()
  }

  constructor(private readonly themeService: ThemeService) {
    super();
  }
}

Bei Betrachtung des Problems im Debugger haben wir festgestellt, dass die Klasse ThemeService als Injected Service gar nicht geladen wurde.

@Injectable({
  providedIn: 'root'
})
export class ThemeService {
    getUserPreference(): Theme {
        return (localStorage.getItem(localStorageThemeKey) ?? 'light') as Theme;
    }

    setUserPreference(theme: Theme): void {
        localStorage.setItem(localStorageThemeKey, theme);
    }
}

Die Lösung

Dies liess uns darauf schliessen, dass es sich eventuell um einen Fehler beim Kompilieren von TypeScript handelt. Wir haben anschliessend einen Abgleich des tsconfig.json mit einem zweiten Angular Projekt vorgenommen, das wir bereits erfolgreich auf Angular 15 aktualisiert hatten. Der wesentlichste Unterschied war die Konfiguration "useDefineForClassFields": false im tsconfig.json.

{
  "compilerOptions": {
    "useDefineForClassFields": false
  }
}

In der Dokumentation von TypeScript haben wir folgende Information dazu gefunden:

# Use Define For Class Fields - useDefineForClassFields useDefineForClassFields This flag is used as part of migrating to the upcoming standard version of class fields. TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the upcoming specification has a different runtime behavior to TypeScript’s implementation but the same syntax.

This flag switches to the upcoming ECMA runtime behavior.

Mit dem wichtigen Zusatz:

Default: true if target is ES2022 or higher, including ESNext,false otherwise.

Nach dem Update auf Angular 15 wurde der target Wert im tsconfig.json durch das Angular-CLI automatisch auf ES2022 gesetzt. Wegen des veränderten Runtime Behaviors für Class Fields hatten wir somit die Probleme mit Injected Services, welche von Class Fields aufgerufen wurden.

Bei einem vollständigen Lauf von ng update @angular/core@latest @angular/cli@latest sollte diese Einstellung automatisch hinzugefügt werden.

Mit der von uns manuell angepassten Konfiguration im tsconfig.json hat nach dem Update auf Angular 15 in unserer Applikation wieder alles so funktioniert, wie erwartet.