Dependency Injection Lanjutan di Angular 2026: Providers, Tokens, dan Pertanyaan Interview

Pelajari sistem dependency injection Angular secara mendalam termasuk strategi provider, InjectionToken, injector hierarkis, dan pertanyaan interview umum untuk developer Angular berpengalaman.

Angular Dependency Injection Advanced Providers Tokens

Sistem dependency injection Angular memberikan kontrol yang presisi terhadap instansiasi service melalui providers, tokens, dan injector hierarkis. Penguasaan konsep-konsep ini membedakan developer Angular berpengalaman dari junior dalam technical interview maupun kode produksi.

Konsep Kunci

Angular memelihara dua pohon injector paralel: pohon ModuleInjector untuk service yang disediakan di level module, dan pohon ElementInjector untuk dependensi yang di-scope ke komponen. Resolusi dimulai dari level elemen dan naik ke root.

Strategi Konfigurasi Provider di Angular DI

Angular menawarkan berbagai strategi konfigurasi provider, masing-masing cocok untuk use case yang berbeda. Konfigurasi paling umum menggunakan useClass, useValue, useFactory, dan 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: provide a different implementation
    { provide: LoggerService, useClass: DebugLoggerService },
    
    // useValue: provide a static configuration object
    { 
      provide: API_CONFIG, 
      useValue: { baseUrl: 'https://api.example.com', timeout: 5000 } 
    },
    
    // useFactory: create dependency with runtime logic
    {
      provide: 'FEATURE_FLAGS',
      useFactory: () => {
        const env = import.meta.env.MODE;
        return { debugMode: env === 'development', analytics: env === 'production' };
      }
    },
    
    // useExisting: create an alias to another provider
    { provide: 'Logger', useExisting: LoggerService }
  ]
};

Strategi useClass menukar implementasi tanpa mengubah kode consumer. Strategi useValue menyediakan objek statis seperti konfigurasi. Strategi useFactory menangani keputusan runtime, dan useExisting membuat alias untuk akses polimorfik.

InjectionToken untuk Dependensi Non-Class yang Type-Safe

Sementara @Injectable bekerja untuk service berbasis class, nilai non-class seperti objek konfigurasi, primitive, atau fungsi memerlukan InjectionToken. Token ini bertindak sebagai kunci unik di registry DI Angular.

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

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

// Generic parameter ensures type safety at injection point
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config', {
  providedIn: 'root',
  factory: () => ({
    baseUrl: 'https://api.sharpskill.dev',
    timeout: 30000,
    retryAttempts: 3
  })
});

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

Parameter tipe generic pada InjectionToken<ApiConfig> merambat ke pemanggilan inject(). TypeScript mengetahui nilai yang di-inject sesuai dengan tipe yang dideklarasikan, menangkap kesalahan penggunaan pada waktu kompilasi bukan runtime.

Fungsi inject() vs Constructor Injection

Angular 14 memperkenalkan fungsi inject() sebagai alternatif dari injection berbasis constructor. Di Angular 20+, inject() telah menjadi pendekatan yang lebih disukai, terutama di standalone component dan konteks fungsional.

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 {
  // Modern approach: inject() at field level
  private readonly http = inject(HttpClient);
  private readonly config = inject(API_CONFIG);

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

// Alternative: constructor injection (still valid)
@Injectable({ providedIn: 'root' })
export class UserServiceLegacy {
  constructor(
    private readonly http: HttpClient,
    @Inject(API_CONFIG) private readonly config: ApiConfig
  ) {}
}

Fungsi inject() menghilangkan boilerplate decorator untuk token dan memungkinkan dependency injection dalam konteks non-class seperti functional guard, resolver, dan interceptor.

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

// Functional guard using inject()
export const authGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

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

Fungsi inject() hanya bekerja dalam injection context: selama konstruksi class, dalam factory function, atau dalam konstruksi Angular fungsional. Memanggilnya di luar konteks ini akan melempar runtime error.

Siap menguasai wawancara Angular Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Injector Hierarkis: ElementInjector vs EnvironmentInjector

Angular memelihara dua hierarki injector paralel yang menentukan scope dan urutan resolusi service. Memahami arsitektur ini sangat penting untuk mengontrol lifetime dan visibilitas service.

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

// Root-level singleton: single instance across entire 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); }
}

// Component-scoped: new instance per component
@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,
  // This component and all children get the same instance
  providers: [ComponentDataService],
  template: `
    <app-widget />
    <app-stats />
  `
})
export class DashboardComponent {}

Ketika komponen mendeklarasikan provider, Angular membuat instance baru yang di-scope ke ElementInjector komponen tersebut. Komponen anak mewarisi akses ke provider parent kecuali mereka mendeklarasikan provider mereka sendiri.

Algoritma resolusi mengikuti jalur ini:

  1. Periksa ElementInjector komponen yang meminta
  2. Naik ke atas pohon ElementInjector ke ancestor
  3. Periksa EnvironmentInjector (module atau standalone provider)
  4. Naik ke root EnvironmentInjector
  5. Mencapai NullInjector dan throw error jika @Optional() tidak digunakan

Modifier Resolusi: @Self, @SkipSelf, @Optional, @Host

Modifier resolusi mengubah cara Angular mencari hierarki injector. Decorator ini bekerja dengan constructor injection maupun fungsi 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: only look in this component's injector, fail otherwise
  private readonly localService = inject(PanelService, { self: true });
  
  // @SkipSelf: skip this component, start search from parent
  private readonly parentService = inject(PanelService, { 
    skipSelf: true, 
    optional: true 
  });
  
  // @Optional: return null instead of throwing if not found
  private readonly optionalService = inject(PanelService, { optional: true });

  constructor() {
    // localService is always the component's own instance
    // parentService is the parent's instance or null
    console.log('Local:', this.localService);
    console.log('Parent:', this.parentService);
  }
}

Modifier @Host() membatasi resolusi ke injector host element dan berhenti di batas komponen. Ini berguna ketika directive perlu mengakses service yang disediakan oleh host component-nya tetapi tidak boleh mencapai lebih tinggi di pohon.

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

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  // Only look at the host component's providers
  private readonly config = inject(HighlightConfig, { 
    host: true, 
    optional: true 
  });

  constructor() {
    // config is null if host component didn't provide HighlightConfig
    const color = this.config?.color ?? 'yellow';
    // Apply highlighting...
  }
}

Multi-Provider untuk Sistem yang Extensible

Multi-provider memungkinkan beberapa nilai didaftarkan di bawah satu token. Angular mengembalikan semua nilai terdaftar sebagai array, memungkinkan arsitektur plugin dan pola extensibility.

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 is an array of all registered validators
    return this.validators
      .map(v => v.validate(value))
      .filter((error): error is string => error !== null);
  }
}

Flag multi: true memberitahu Angular untuk mengumpulkan semua provider untuk token ini ke dalam array. Tanpanya, provider selanjutnya akan menimpa yang sebelumnya.

Pertanyaan Interview tentang Angular Dependency Injection

Technical interview sering menguji pemahaman tentang sistem DI Angular. Berikut pertanyaan yang membedakan kandidat dengan pengalaman produksi.

Q: Apa yang terjadi ketika Anda menyediakan service yang sama di level module dan component?

Provider level komponen membuat instance terpisah yang di-scope ke subtree komponen tersebut. Service yang di-inject di subtree tersebut menerima instance komponen, bukan singleton level module. Ini memungkinkan isolasi state, misalnya ketika setiap tab memerlukan state form-nya sendiri.

Q: Mengapa menggunakan InjectionToken daripada string literal?

String token berisiko collision di seluruh library atau bagian aplikasi yang berbeda. InjectionToken membuat referensi runtime unik yang tidak bisa konflik. Parameter tipe generic juga menyediakan keamanan tipe compile-time yang tidak dimiliki string token.

Q: Kapan inject() throw vs return undefined?

Secara default, inject() throw ketika dependensi tidak ditemukan. Memberikan { optional: true } mengubah return type menjadi T | null dan mengembalikan null alih-alih throw. Ini sesuai dengan perilaku decorator @Optional().

Q: Jelaskan perbedaan antara providedIn: 'root' dan providing di array providers module.

Keduanya membuat singleton, tetapi providedIn: 'root' memungkinkan tree-shaking. Service hanya disertakan dalam bundle jika benar-benar di-inject di suatu tempat. Provider level module selalu disertakan terlepas dari penggunaan.

Q: Bagaimana lazy loading mempengaruhi scope service?

Lazy-loaded module mendapatkan child EnvironmentInjector mereka sendiri. Service yang disediakan di lazy module di-scope ke module tersebut dan children-nya. Service dengan providedIn: 'root' tetap menjadi singleton sejati di semua module, lazy maupun tidak.

Untuk latihan pertanyaan tentang service Angular dan pola DI, kunjungi pertanyaan interview Angular tentang service dan dependency injection.

Pola Praktis: Konfigurasi dan Feature Flag

Aplikasi nyata menggabungkan konsep DI ini untuk manajemen konfigurasi. Pola ini menggunakan InjectionToken, factory provider, dan kesadaran environment.

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: return safe defaults
      return { newCheckout: false, darkMode: false, betaFeatures: false };
    }

    // Browser: check localStorage or remote config
    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();
    }
  }
}

Directive ini secara kondisional merender konten berdasarkan feature flag, dengan konfigurasi flag tersentralisasi dalam satu injectable token.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Praktik Angular DI Production-Ready

  • Gunakan providedIn: 'root' untuk singleton application-wide yang mendapat manfaat dari tree-shaking
  • Lebih pilih inject() daripada constructor injection di Angular 20+ untuk sintaks yang lebih bersih dan kompatibilitas fungsional
  • Scope stateful service ke komponen ketika isolasi diperlukan, bukan di level module
  • Buat InjectionToken untuk dependensi non-class untuk memastikan keamanan tipe dan menghindari collision
  • Terapkan @Optional() ketika dependensi mungkin tidak ada, terutama untuk plugin atau fitur opsional
  • Test komponen dengan overridden provider menggunakan TestBed.overrideComponent() untuk isolasi
  • Gunakan multi-provider untuk pola extensibility seperti validator, interceptor, dan handler
Tantangan harian

Bisakah kamu menemukan bug di Angular?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 13 September 2026

Tag

#angular
#dependency-injection
#typescript
#interview

Bagikan

Artikel terkait