Angular 17 + NgRx 15 で発生したエラー
TypeError: Cannot read properties of undefined (reading 'pipe')
は フィールド初期化子がコンストラクタの依存注入より先に評価される という順序の落とし穴が原因。inject() を使って依存を先に解決することで解消できます。
1. 目的
本ドキュメントは、同種の初期化順序バグをチーム全体で共有し再発を防止することを目的とします。エラーの再現条件、根本原因、解決策、そしてバージョン依存性を整理しました。
2. 発生したエラー
2.1 実際のスタックトレース
main.ts:7 TypeError: Cannot read properties of undefined (reading 'pipe')
at auth.effects.ts:19:19
at createEffect (ngrx-effects.mjs:85:47)
at <instance_members_initializer> (auth.effects.ts:18:12)
at new _AuthEffects (auth.effects.ts:12:3)
at Object.AuthEffects_Factory [as factory] (auth.effects.ts:64:24)
at core.mjs:2243:35
...(以下略)
2.2 発生環境
ライブラリ | バージョン |
---|---|
Angular | 17.0.x |
@ngrx/effects | 15.3.x |
@ngrx/store | 15.3.x |
TypeScript | 5.4.x |
3. 原因分析
3.1 フィールド初期化子 vs パラメータプロパティ
TypeScript は クラスフィールド初期化子 をコンストラクタの 冒頭 に展開します。
一方、コンストラクタの パラメータプロパティ はその後で this.x = x を実行します。
したがって、下記のような順序になります:
constructor(private actions$: Actions) {}
login$ = createEffect(() => this.actions$.pipe(...));
// 生成されるおおよその JavaScript
constructor(actions$) {
this.login$ = createEffect(() => this.actions$.pipe(...)); // ← undefined 参照
this.actions$ = actions$; // 後から代入
}
3.2 NgRx createEffect の実装詳細
NgRx v15 以降では メタデータ収集時に sourceFactory() が即時実行 される。
そのためフィールド評価の段階で this.actions$ が未定義だと、undefined.pipe 例外が即発生する。
4. 解決策(変更点)
4.1 inject() による依存解決
import { inject, Injectable } from '@angular/core';
@Injectable()
export class AuthEffects {
private actions$ = inject(Actions); // ① 先に確定
private auth = inject(AuthService);
private router = inject(Router);
login$ = createEffect(() => {
return this.actions$.pipe(/* ... */);
}); // ② 依存が揃った後に実行
}
4.2 Before / After Diff
- constructor(
- private actions$: Actions,
- private auth: AuthService,
- private router: Router
- ) {}
-
- login$ = createEffect(() => this.actions$.pipe(...));
+ private actions$ = inject(Actions);
+ private auth = inject(AuthService);
+ private router = inject(Router);
+
+ login$ = createEffect(() => this.actions$.pipe(...));
5. バージョン互換と注意点
Angular | NgRx | 状況 |
---|---|---|
≤ 15 | ≤14 | inject() は未サポート。コンストラクタ内で createEffect を呼び出す or フィールドで依存を使わないこと。 |
16 | 15 | inject() 利用可能。ただし Signal API と併用時は order to update loop に注意。 |
17+ | 15+ | inject() + フィールド定義が推奨パターン。今回の順序バグは自然に回避できる。 |
6. 追加参考資料
NgRx Docs: createEffect API
Angular Docs: Dependency Injection with inject()
RFC: Injectable Composition API
8. まとめ
教訓: フィールドで依存を使うなら、依存を先に解決する。
Angular 17 / NgRx 15 以降は inject() + フィールド宣言順のパターンを採用することで、今回のような初期化順序バグを根本的に防止できます。今後は新規 Effect 実装/リファクタ時に必ずこのパターンを徹底しましょう。