Dependency Injection Avanzata in Angular 2026: Provider, Token e Domande da Colloquio

Il sistema di dependency injection di Angular offre un controllo preciso sull'istanziazione dei servizi attraverso provider, token e iniettori gerarchici. Questa analisi approfondita esplora le strategie dei provider, la funzione inject() e le domande tecniche da colloquio.

Dependency Injection Avanzata in Angular 2026: Provider, Token e Domande da Colloquio

Il sistema di dependency injection di Angular garantisce un controllo granulare sull'istanziazione dei servizi attraverso provider, token e iniettori gerarchici. La padronanza di questi concetti distingue gli sviluppatori Angular esperti dai principianti nei colloqui tecnici e nel codice di produzione.

Concetto Chiave

Angular mantiene due alberi di iniettori paralleli: l'albero ModuleInjector per i servizi forniti a livello di modulo e l'albero ElementInjector per le dipendenze a livello di componente. La risoluzione inizia a livello di elemento e risale verso la radice.

Strategie di Configurazione dei Provider in Angular DI

Angular offre diverse strategie di configurazione dei provider, ciascuna adatta a casi d'uso specifici. Le configurazioni più comuni utilizzano useClass, useValue, useFactory e useExisting.

app.config.tstypescript
import { ApplicationConfig, InjectionToken } from '@angular/core';
import { LoggerService } from './services/logger.service';
import { DebugLoggerService } from './services/debug-logger.service';
import { API_CONFIG, ApiConfig } from './config/api.config';

export const appConfig: ApplicationConfig = {
  providers: [
    // useClass: fornire un'implementazione diversa
    { provide: LoggerService, useClass: DebugLoggerService },
    
    // useValue: fornire un oggetto di configurazione statico
    { 
      provide: API_CONFIG, 
      useValue: { baseUrl: 'https://api.example.com', timeout: 5000 } 
    },
    
    // useFactory: creare dipendenza con logica runtime
    {
      provide: 'FEATURE_FLAGS',
      useFactory: () => {
        const env = import.meta.env.MODE;
        return { debugMode: env === 'development', analytics: env === 'production' };
      }
    },
    
    // useExisting: creare un alias verso un altro provider
    { provide: 'Logger', useExisting: LoggerService }
  ]
};

La strategia useClass sostituisce le implementazioni senza modificare il codice consumer. La strategia useValue fornisce oggetti statici come configurazioni. La strategia useFactory gestisce decisioni runtime, mentre useExisting crea alias per l'accesso polimorfico.

InjectionToken per Dipendenze Non-Classe Type-Safe

Mentre @Injectable funziona per servizi basati su classi, valori non-classe come oggetti di configurazione, primitive o funzioni richiedono un InjectionToken. Questo token funge da chiave univoca nel registro DI di Angular.

tokens/config.tokens.tstypescript
import { InjectionToken } from '@angular/core';

export interface ApiConfig {
  baseUrl: string;
  timeout: number;
  retryAttempts: number;
}

// Il parametro generico garantisce type safety al punto di iniezione
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config', {
  providedIn: 'root',
  factory: () => ({
    baseUrl: 'https://api.sharpskill.dev',
    timeout: 30000,
    retryAttempts: 3
  })
});

// Token per valori primitivi
export const MAX_UPLOAD_SIZE = new InjectionToken<number>('max.upload.size', {
  providedIn: 'root',
  factory: () => 10 * 1024 * 1024 // 10MB
});

Il parametro di tipo generico su InjectionToken<ApiConfig> si propaga alla chiamata inject(). TypeScript riconosce che il valore iniettato corrisponde al tipo dichiarato, intercettando errori in fase di compilazione anziché a runtime.

La Funzione inject() vs Iniezione via Costruttore

Angular 14 ha introdotto la funzione inject() come alternativa all'iniezione basata su costruttore. In Angular 20+, inject() è diventato l'approccio preferito, specialmente nei componenti standalone e nei contesti funzionali.

user.service.tstypescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { API_CONFIG } from '../tokens/config.tokens';

@Injectable({ providedIn: 'root' })
export class UserService {
  // Approccio moderno: inject() a livello di campo
  private readonly http = inject(HttpClient);
  private readonly config = inject(API_CONFIG);

  getUser(id: string) {
    return this.http.get(`${this.config.baseUrl}/users/${id}`);
  }
}

// Alternativa: iniezione via costruttore (ancora valida)
@Injectable({ providedIn: 'root' })
export class UserServiceLegacy {
  constructor(
    private readonly http: HttpClient,
    @Inject(API_CONFIG) private readonly config: ApiConfig
  ) {}
}

La funzione inject() elimina il boilerplate dei decorator per i token e abilita la dependency injection in contesti non-classe come guard funzionali, resolver e interceptor.

auth.guard.tstypescript
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

// Guard funzionale con inject()
export const authGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true;
  }
  return router.createUrlTree(['/login']);
};

La funzione inject() funziona solo all'interno di un contesto di iniezione: durante la costruzione della classe, nelle funzioni factory o nei costrutti funzionali Angular. Chiamarla al di fuori di questi contesti genera un errore runtime.

Pronto a superare i tuoi colloqui su Angular?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Iniettori Gerarchici: ElementInjector vs EnvironmentInjector

Angular mantiene due gerarchie di iniettori parallele che determinano lo scope dei servizi e l'ordine di risoluzione. Comprendere questa architettura è essenziale per controllare la durata e la visibilità dei servizi.

data.service.tstypescript
import { Injectable } from '@angular/core';

// Singleton a livello root: singola istanza per l'intera app
@Injectable({ providedIn: 'root' })
export class GlobalDataService {
  private data = new Map<string, unknown>();
  
  set(key: string, value: unknown) { this.data.set(key, value); }
  get(key: string) { return this.data.get(key); }
}

// Scope componente: nuova istanza per componente
@Injectable()
export class ComponentDataService {
  private data = new Map<string, unknown>();
  
  set(key: string, value: unknown) { this.data.set(key, value); }
  get(key: string) { return this.data.get(key); }
}
dashboard.component.tstypescript
import { Component } from '@angular/core';
import { ComponentDataService } from './services/component-data.service';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  // Questo componente e tutti i figli ottengono la stessa istanza
  providers: [ComponentDataService],
  template: `
    <app-widget />
    <app-stats />
  `
})
export class DashboardComponent {}

Quando un componente dichiara un provider, Angular crea una nuova istanza con scope nell'ElementInjector di quel componente. I componenti figli ereditano l'accesso ai provider del genitore a meno che non ne dichiarino di propri.

L'algoritmo di risoluzione segue questo percorso:

  1. Controllare l'ElementInjector del componente richiedente
  2. Risalire l'albero ElementInjector verso gli antenati
  3. Controllare l'EnvironmentInjector (provider di modulo o standalone)
  4. Risalire al root EnvironmentInjector
  5. Raggiungere NullInjector e lanciare errore se @Optional() non è stato usato

Modificatori di Risoluzione: @Self, @SkipSelf, @Optional, @Host

I modificatori di risoluzione alterano il modo in cui Angular cerca nella gerarchia degli iniettori. Questi decorator funzionano sia con l'iniezione via costruttore che con la funzione inject().

panel.component.tstypescript
import { Component, Optional, SkipSelf, Self, inject } from '@angular/core';
import { PanelService } from './panel.service';

@Component({
  selector: 'app-panel',
  standalone: true,
  providers: [PanelService],
  template: `<ng-content />`
})
export class PanelComponent {
  // @Self: cercare solo nell'iniettore di questo componente, altrimenti fallire
  private readonly localService = inject(PanelService, { self: true });
  
  // @SkipSelf: saltare questo componente, iniziare la ricerca dal genitore
  private readonly parentService = inject(PanelService, { 
    skipSelf: true, 
    optional: true 
  });
  
  // @Optional: restituire null invece di lanciare errore se non trovato
  private readonly optionalService = inject(PanelService, { optional: true });

  constructor() {
    // localService è sempre l'istanza propria del componente
    // parentService è l'istanza del genitore o null
    console.log('Local:', this.localService);
    console.log('Parent:', this.parentService);
  }
}

Il modificatore @Host() limita la risoluzione all'iniettore dell'elemento host e si ferma ai confini del componente. Questo è utile quando una direttiva deve accedere a un servizio fornito dal suo componente host ma non deve cercare più in alto nell'albero.

highlight.directive.tstypescript
import { Directive, inject, Host, Optional } from '@angular/core';
import { HighlightConfig } from './highlight.config';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  // Cercare solo nei provider del componente host
  private readonly config = inject(HighlightConfig, { 
    host: true, 
    optional: true 
  });

  constructor() {
    // config è null se il componente host non fornisce HighlightConfig
    const color = this.config?.color ?? 'yellow';
    // Applicare evidenziazione...
  }
}

Multi-Provider per Sistemi Estensibili

I multi-provider permettono di registrare più valori sotto un singolo token. Angular restituisce tutti i valori registrati come array, abilitando architetture a plugin e pattern di estensibilità.

validators.tokens.tstypescript
import { InjectionToken } from '@angular/core';

export interface Validator {
  validate(value: string): string | null;
}

export const VALIDATORS = new InjectionToken<Validator[]>('validators');
app.config.tstypescript
import { ApplicationConfig } from '@angular/core';
import { VALIDATORS } from './validators.tokens';

const requiredValidator = {
  validate: (value: string) => value ? null : 'Field is required'
};

const minLengthValidator = {
  validate: (value: string) => value.length >= 3 ? null : 'Minimum 3 characters'
};

const emailValidator = {
  validate: (value: string) => 
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email format'
};

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: VALIDATORS, useValue: requiredValidator, multi: true },
    { provide: VALIDATORS, useValue: minLengthValidator, multi: true },
    { provide: VALIDATORS, useValue: emailValidator, multi: true }
  ]
};
validation.service.tstypescript
import { Injectable, inject } from '@angular/core';
import { VALIDATORS, Validator } from './validators.tokens';

@Injectable({ providedIn: 'root' })
export class ValidationService {
  private readonly validators = inject(VALIDATORS);

  validate(value: string): string[] {
    // validators è un array di tutti i validatori registrati
    return this.validators
      .map(v => v.validate(value))
      .filter((error): error is string => error !== null);
  }
}

Il flag multi: true indica ad Angular di raccogliere tutti i provider per questo token in un array. Senza di esso, i provider successivi sovrascriverebbero quelli precedenti.

Domande da Colloquio sulla Dependency Injection di Angular

I colloqui tecnici spesso esplorano la comprensione del sistema DI di Angular. Ecco domande che distinguono i candidati con esperienza in produzione.

D: Cosa succede quando lo stesso servizio viene fornito sia a livello di modulo che di componente?

Il provider a livello di componente crea un'istanza separata con scope nel sottoalbero di quel componente. I servizi iniettati in quel sottoalbero ricevono l'istanza del componente, non il singleton a livello di modulo. Questo permette l'isolamento dello stato, ad esempio quando ogni tab necessita del proprio stato del form.

D: Perché usare InjectionToken invece di una stringa letterale?

I token stringa rischiano collisioni tra librerie o parti diverse di un'applicazione. InjectionToken crea un riferimento runtime univoco che non può collidere. Il parametro di tipo generico fornisce anche type safety in fase di compilazione che manca ai token stringa.

D: Quando inject() lancia un errore vs restituisce undefined?

Per default, inject() lancia un errore quando la dipendenza non viene trovata. Passando { optional: true } si cambia il tipo di ritorno in T | null e si restituisce null invece di lanciare l'errore. Questo corrisponde al comportamento del decorator @Optional().

D: Spiegare la differenza tra providedIn: 'root' e fornire nell'array providers di un modulo.

Entrambi creano singleton, ma providedIn: 'root' abilita il tree-shaking. Il servizio viene incluso nel bundle solo se effettivamente iniettato da qualche parte. I provider a livello di modulo sono sempre inclusi indipendentemente dall'uso.

D: Come il lazy loading influenza lo scope dei servizi?

I moduli caricati in lazy loading ottengono il proprio EnvironmentInjector figlio. I servizi forniti in un modulo lazy hanno scope limitato a quel modulo e ai suoi figli. Un servizio con providedIn: 'root' rimane un vero singleton attraverso tutti i moduli, lazy o meno.

Per domande di pratica sui servizi Angular e pattern DI, consultare Domande da colloquio Angular su servizi e dependency injection.

Pattern Pratici: Configurazione e Feature Flag

Le applicazioni reali combinano questi concetti DI per la gestione della configurazione. Questo pattern utilizza InjectionToken, factory provider e consapevolezza dell'ambiente.

feature-flags.config.tstypescript
import { InjectionToken, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

export interface FeatureFlags {
  newCheckout: boolean;
  darkMode: boolean;
  betaFeatures: boolean;
}

export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('feature.flags', {
  providedIn: 'root',
  factory: () => {
    const platformId = inject(PLATFORM_ID);
    
    if (!isPlatformBrowser(platformId)) {
      // SSR: restituire valori di default sicuri
      return { newCheckout: false, darkMode: false, betaFeatures: false };
    }

    // Browser: controllare localStorage o config remota
    const stored = localStorage.getItem('featureFlags');
    if (stored) {
      return JSON.parse(stored);
    }
    
    return { newCheckout: true, darkMode: false, betaFeatures: false };
  }
});
feature-flag.directive.tstypescript
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
import { FEATURE_FLAGS } from './feature-flags.config';

@Directive({
  selector: '[appFeatureFlag]',
  standalone: true
})
export class FeatureFlagDirective {
  private readonly flags = inject(FEATURE_FLAGS);
  private readonly templateRef = inject(TemplateRef<unknown>);
  private readonly viewContainer = inject(ViewContainerRef);

  @Input() set appFeatureFlag(flag: keyof typeof this.flags) {
    if (this.flags[flag]) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

Questa direttiva renderizza contenuto condizionalmente basandosi sui feature flag, con la configurazione dei flag centralizzata in un token iniettabile.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Best Practice Angular DI per la Produzione

  • Usare providedIn: 'root' per singleton application-wide che beneficiano del tree-shaking
  • In Angular 20+ preferire inject() all'iniezione via costruttore per sintassi più pulita e compatibilità funzionale
  • Limitare i servizi stateful ai componenti quando serve isolamento, non a livello di modulo
  • Creare InjectionToken per dipendenze non-classe per garantire type safety ed evitare collisioni
  • Applicare @Optional() quando una dipendenza potrebbe non esistere, specialmente per plugin o feature opzionali
  • Testare componenti con provider sovrascritti usando TestBed.overrideComponent() per l'isolamento
  • Usare multi-provider per pattern di estensibilità come validatori, interceptor e handler
Sfida del giorno

Sapresti trovare il bug in Angular?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 13 settembre 2026

Tag

#angular
#dependency injection
#providers
#injection token
#interview

Condividi

Articoli correlati