2026年版 Angular依存性注入の完全ガイド: プロバイダー、トークン、面接対策

Angularの高度な依存性注入(DI)システムを徹底解説。InjectionToken、階層型インジェクター、プロバイダー戦略、そして技術面接で頻出する質問と回答例を網羅的に紹介します。

Angular依存性注入の高度なパターンを示す図解

Angularの依存性注入(Dependency Injection、以下DI)システムは、フレームワークの中核を成す機能です。2026年現在、Angular 19ではinject関数やシグナルベースの状態管理との統合により、DIの活用方法がさらに進化しています。本記事では、上級開発者が押さえるべきDIの高度なパターンと、技術面接で頻出する質問について詳しく解説します。

依存性注入の基本概念を理解していることを前提としています。初学者の方は、まず公式ドキュメントの入門ガイドを確認することをお勧めします。

InjectionTokenによる型安全な依存性管理

文字列ベースのトークンは名前衝突のリスクがあり、型安全性も保証されません。InjectionTokenを使用することで、これらの問題を解決できます。

typescript
import { InjectionToken, inject } from '@angular/core';

export interface AppConfig {
  apiUrl: string;
  featureFlags: {
    darkMode: boolean;
    betaFeatures: boolean;
  };
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', {
  providedIn: 'root',
  factory: () => ({
    apiUrl: 'https://api.example.com',
    featureFlags: {
      darkMode: true,
      betaFeatures: false,
    },
  }),
});

// コンポーネントでの使用
export class FeatureComponent {
  private config = inject(APP_CONFIG);

  get apiEndpoint(): string {
    return this.config.apiUrl;
  }
}

factory関数を指定することで、トークンにデフォルト値を設定できます。これにより、明示的なプロバイダー設定がない場合でも、安全にサービスを利用できます。

階層型インジェクターの仕組み

AngularのDIシステムは階層構造を持ち、複数のインジェクターが木構造で連携します。この仕組みを理解することは、大規模アプリケーションの設計において極めて重要です。

typescript
import { Injectable, inject } from '@angular/core';

@Injectable()
export class LoggerService {
  private prefix = 'DEFAULT';

  setPrefix(prefix: string): void {
    this.prefix = prefix;
  }

  log(message: string): void {
    console.log(`[${this.prefix}] ${message}`);
  }
}

// 親コンポーネント
@Component({
  selector: 'app-parent',
  providers: [LoggerService],
  template: `<app-child />`,
})
export class ParentComponent {
  private logger = inject(LoggerService);

  constructor() {
    this.logger.setPrefix('PARENT');
  }
}

// 子コンポーネント - 親のインスタンスを継承
@Component({
  selector: 'app-child',
  template: `<button (click)="logMessage()">Log</button>`,
})
export class ChildComponent {
  private logger = inject(LoggerService);

  logMessage(): void {
    this.logger.log('Message from child'); // [PARENT] Message from child
  }
}

子コンポーネントに独自のprovidersを設定すると、新しいインスタンスが生成され、親とは異なるスコープで動作します。

プロバイダー戦略の使い分け

Angularでは複数のプロバイダー設定方法があり、それぞれ異なるユースケースに適しています。

useClassによる実装の切り替え

typescript
import { Injectable } from '@angular/core';

export abstract class NotificationService {
  abstract send(message: string): void;
}

@Injectable()
export class EmailNotificationService extends NotificationService {
  send(message: string): void {
    console.log(`Email: ${message}`);
  }
}

@Injectable()
export class PushNotificationService extends NotificationService {
  send(message: string): void {
    console.log(`Push: ${message}`);
  }
}

// モジュールでの設定
@NgModule({
  providers: [
    {
      provide: NotificationService,
      useClass: environment.mobile
        ? PushNotificationService
        : EmailNotificationService,
    },
  ],
})
export class AppModule {}

useFactoryによる動的生成

typescript
import { InjectionToken, inject } from '@angular/core';

export const API_CLIENT = new InjectionToken<ApiClient>('api.client', {
  providedIn: 'root',
  factory: () => {
    const config = inject(APP_CONFIG);
    const http = inject(HttpClient);

    return new ApiClient(http, {
      baseUrl: config.apiUrl,
      timeout: 30000,
      retryAttempts: 3,
    });
  },
});

useExistingによるエイリアス

typescript
@NgModule({
  providers: [
    LoggerService,
    {
      provide: 'LEGACY_LOGGER',
      useExisting: LoggerService,
    },
  ],
})
export class CoreModule {}

この設定により、新旧両方のトークンで同じインスタンスにアクセスできます。レガシーコードの段階的な移行に有効です。

inject関数とコンストラクター注入の比較

Angular 14以降で導入されたinject関数は、コンストラクター注入に代わる新しいパターンです。

typescript
// inject関数を使用したパターン
@Component({
  selector: 'app-modern',
  template: `...`,
})
export class ModernComponent {
  private userService = inject(UserService);
  private router = inject(Router);
  private config = inject(APP_CONFIG);

  // ロジックをすぐに記述可能
}

// コンストラクター注入パターン
@Component({
  selector: 'app-traditional',
  template: `...`,
})
export class TraditionalComponent {
  constructor(
    private userService: UserService,
    private router: Router,
    @Inject(APP_CONFIG) private config: AppConfig
  ) {}
}

inject関数の利点は、継承時のボイラープレートコード削減と、フィールドイニシャライザーでの直接利用です。ただし、injectは注入コンテキスト内でのみ使用可能という制約があります。

環境別インジェクターの活用

Angular 19では、EnvironmentInjectorを使用して動的なコンポーネント生成やサービス解決を行えます。

typescript
import {
  EnvironmentInjector,
  createEnvironmentInjector,
  inject,
} from '@angular/core';

@Injectable({ providedIn: 'root' })
export class PluginLoaderService {
  private parentInjector = inject(EnvironmentInjector);

  loadPlugin(pluginConfig: PluginConfig): EnvironmentInjector {
    return createEnvironmentInjector(
      [
        {
          provide: PLUGIN_CONFIG,
          useValue: pluginConfig,
        },
        PluginService,
      ],
      this.parentInjector
    );
  }
}

このパターンは、マイクロフロントエンドアーキテクチャやプラグインシステムの実装で威力を発揮します。

マルチプロバイダーによる拡張可能な設計

multi: trueオプションを使用すると、同じトークンに複数の値を登録できます。

typescript
import { InjectionToken, inject } from '@angular/core';

export interface Validator {
  validate(value: unknown): boolean;
  errorMessage: string;
}

export const VALIDATORS = new InjectionToken<Validator[]>('validators');

// モジュールでの登録
@NgModule({
  providers: [
    {
      provide: VALIDATORS,
      useClass: RequiredValidator,
      multi: true,
    },
    {
      provide: VALIDATORS,
      useClass: EmailValidator,
      multi: true,
    },
    {
      provide: VALIDATORS,
      useClass: MinLengthValidator,
      multi: true,
    },
  ],
})
export class ValidationModule {}

// サービスでの利用
@Injectable({ providedIn: 'root' })
export class ValidationService {
  private validators = inject(VALIDATORS);

  validateAll(value: unknown): string[] {
    return this.validators
      .filter((v) => !v.validate(value))
      .map((v) => v.errorMessage);
  }
}

HTTPインターセプターやルートガードも、この仕組みを内部的に利用しています。

オプショナル依存性と@Self、@SkipSelf

依存性が存在しない場合の挙動を制御するデコレーターを理解することは重要です。

typescript
import { Injectable, Optional, Self, SkipSelf, inject } from '@angular/core';

@Component({
  selector: 'app-example',
  template: `...`,
})
export class ExampleComponent {
  // 存在しない場合はnull
  private optionalService = inject(OptionalService, { optional: true });

  // 自身のインジェクターのみを検索
  private selfService = inject(SelfService, { self: true, optional: true });

  // 親のインジェクターから検索開始
  private parentService = inject(ParentService, { skipSelf: true });
}

これらのオプションを組み合わせることで、複雑な依存性解決のシナリオに対応できます。

Angularの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

技術面接でよく出る質問と回答例

Q1: providedIn: 'root' と NgModuleのprovidersの違いは?

providedIn: 'root'は、サービスがルートインジェクターに登録され、アプリケーション全体でシングルトンとして共有されます。Tree-shakingが有効になり、使用されないサービスはバンドルから除外されます。一方、NgModuleのproviders配列で登録した場合、そのモジュールがロードされるまでサービスは登録されません。遅延ロードモジュールでは、別のインスタンスが生成される可能性があります。

Q2: InjectionTokenを使用する理由は?

インターフェースや型エイリアスはランタイムに存在しないため、文字列やクラス以外の値をDIトークンとして使用できません。InjectionTokenは、型安全性を維持しながら、任意の値(設定オブジェクト、関数、プリミティブ値など)を注入可能にします。また、名前衝突のリスクを排除し、ファクトリー関数によるデフォルト値の設定も可能です。

Q3: forwardRefはどのような場合に使用しますか?

循環依存や、定義前のクラスを参照する必要がある場合に使用します。TypeScriptのホイスティングにより、クラスが定義される前に参照するとundefinedになる問題を回避できます。

typescript
import { forwardRef, Inject } from '@angular/core';

@Injectable()
export class ServiceA {
  constructor(
    @Inject(forwardRef(() => ServiceB))
    private serviceB: ServiceB
  ) {}
}

@Injectable()
export class ServiceB {
  constructor(private serviceA: ServiceA) {}
}

Q4: コンポーネントレベルでサービスを提供するメリットは?

コンポーネントのproviders配列でサービスを登録すると、コンポーネントごとに新しいインスタンスが生成されます。これにより、状態のカプセル化が実現し、コンポーネントの破棄時にサービスも自動的にクリーンアップされます。リスト項目のような繰り返しコンポーネントで、各項目が独立した状態を持つ必要がある場合に有効です。

Q5: ResolutionModifierの役割を説明してください

@Optional@Self@SkipSelf@Hostは依存性解決の挙動を変更します。@Optionalは依存性が見つからない場合にnullを返し、@Selfは自身のインジェクターのみを検索し、@SkipSelfは親から検索を開始し、@Hostはホストコンポーネントまでの範囲で検索を行います。

まとめ

Angularの依存性注入システムは、単純なサービス共有から複雑なプラグインアーキテクチャまで、幅広いユースケースに対応できる柔軟性を備えています。2026年現在では、inject関数の普及により、より簡潔で型安全なコードが書けるようになりました。

階層型インジェクターの理解、適切なプロバイダー戦略の選択、そしてInjectionTokenの効果的な活用は、保守性の高いAngularアプリケーションを構築するための必須スキルです。技術面接では、これらの概念の理論的理解だけでなく、実際のユースケースに基づいた説明ができることが重要です。

今日のチャレンジ

Angular のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月13日 更新

タグ

#angular
#dependency-injection
#typescript
#interview

共有

関連記事