Dependency Injection ขั้นสูงใน Angular 2026: Providers, Tokens และคำถามสัมภาษณ์

เรียนรู้ระบบ dependency injection ของ Angular อย่างลึกซึ้ง รวมถึงกลยุทธ์ provider, InjectionToken, injector แบบลำดับชั้น และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Angular ที่มีประสบการณ์

Angular Dependency Injection Advanced Providers Tokens

ระบบ dependency injection ของ Angular ให้การควบคุมที่ละเอียดเกี่ยวกับการสร้าง instance ของ service ผ่าน providers, tokens และ injector แบบลำดับชั้น การเชี่ยวชาญแนวคิดเหล่านี้แยกแยะนักพัฒนา Angular ที่มีประสบการณ์จากผู้เริ่มต้นในการสัมภาษณ์ทางเทคนิคและ codebase ระดับ production

แนวคิดหลัก

Angular รักษาสองต้นไม้ injector แบบขนาน: ต้นไม้ ModuleInjector สำหรับ service ที่ให้บริการที่ระดับ module และต้นไม้ ElementInjector สำหรับ dependency ที่มี scope ระดับ component การ resolve เริ่มต้นที่ระดับ element และไล่ขึ้นไปถึง root

กลยุทธ์การกำหนดค่า Provider ใน Angular DI

Angular มีกลยุทธ์การกำหนดค่า provider หลายแบบ แต่ละแบบเหมาะกับกรณีการใช้งานที่แตกต่างกัน การกำหนดค่าที่พบบ่อยที่สุดใช้ useClass, useValue, useFactory และ 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 }
  ]
};

กลยุทธ์ useClass สลับ implementation โดยไม่ต้องเปลี่ยนโค้ด consumer กลยุทธ์ useValue ให้ object แบบ static เช่น configuration กลยุทธ์ useFactory จัดการการตัดสินใจ runtime และ useExisting สร้าง alias สำหรับการเข้าถึงแบบ polymorphic

InjectionToken สำหรับ Dependency Non-Class ที่ Type-Safe

ในขณะที่ @Injectable ทำงานสำหรับ service ที่เป็น class ค่าที่ไม่ใช่ class เช่น object configuration, primitive หรือ function ต้องการ InjectionToken Token นี้ทำหน้าที่เป็น key ที่ไม่ซ้ำกันใน 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
});

พารามิเตอร์ type generic บน InjectionToken<ApiConfig> ส่งต่อไปยังการเรียก inject() TypeScript รู้ว่าค่าที่ inject ตรงกับ type ที่ประกาศ จับข้อผิดพลาดในการใช้งานที่เวลา compile แทนที่จะเป็น runtime

ฟังก์ชัน inject() เทียบกับ Constructor Injection

Angular 14 แนะนำฟังก์ชัน inject() เป็นทางเลือกแทน injection แบบ constructor ใน Angular 20+, inject() กลายเป็นแนวทางที่นิยม โดยเฉพาะใน standalone component และบริบท functional

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
  ) {}
}

ฟังก์ชัน inject() ลด boilerplate decorator สำหรับ token และเปิดใช้งาน dependency injection ในบริบท non-class เช่น functional guard, resolver และ 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']);
};

ฟังก์ชัน inject() ทำงานเฉพาะใน injection context: ระหว่างการสร้าง class, ใน factory function หรือในโครงสร้าง Angular แบบ functional การเรียกนอกบริบทเหล่านี้จะ throw runtime error

พร้อมที่จะพิชิตการสัมภาษณ์ Angular แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Injector แบบลำดับชั้น: ElementInjector vs EnvironmentInjector

Angular รักษาสองลำดับชั้น injector แบบขนานที่กำหนด scope และลำดับการ resolve ของ service การเข้าใจสถาปัตยกรรมนี้จำเป็นสำหรับการควบคุม lifetime และการมองเห็นของ 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 {}

เมื่อ component ประกาศ provider Angular สร้าง instance ใหม่ที่มี scope ไปยัง ElementInjector ของ component นั้น Component ลูกสืบทอดการเข้าถึง provider ของ parent เว้นแต่จะประกาศ provider ของตัวเอง

อัลกอริทึมการ resolve ตามเส้นทางนี้:

  1. ตรวจสอบ ElementInjector ของ component ที่ร้องขอ
  2. เดินขึ้นต้นไม้ ElementInjector ไปยัง ancestor
  3. ตรวจสอบ EnvironmentInjector (module หรือ standalone provider)
  4. เดินขึ้นไปยัง root EnvironmentInjector
  5. ถึง NullInjector และ throw error ถ้าไม่ได้ใช้ @Optional()

Modifier การ Resolve: @Self, @SkipSelf, @Optional, @Host

Modifier การ resolve เปลี่ยนวิธีที่ Angular ค้นหาลำดับชั้น injector Decorator เหล่านี้ทำงานกับทั้ง constructor injection และฟังก์ชัน 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() จำกัดการ resolve ไปยัง injector ของ host element และหยุดที่ขอบเขต component สิ่งนี้มีประโยชน์เมื่อ directive ต้องการเข้าถึง service ที่ให้โดย host component แต่ไม่ควรเอื้อมถึงที่สูงกว่าในต้นไม้

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 สำหรับระบบที่ขยายได้

Multi-provider อนุญาตให้ลงทะเบียนหลายค่าภายใต้ token เดียว Angular คืนค่าทั้งหมดที่ลงทะเบียนเป็น array เปิดใช้งานสถาปัตยกรรม plugin และรูปแบบการขยาย

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 บอก Angular ให้รวบรวม provider ทั้งหมดสำหรับ token นี้เป็น array หากไม่มี provider หลังๆ จะ override ตัวก่อนหน้า

คำถามสัมภาษณ์เกี่ยวกับ Angular Dependency Injection

การสัมภาษณ์ทางเทคนิคมักทดสอบความเข้าใจเกี่ยวกับระบบ DI ของ Angular ต่อไปนี้คือคำถามที่แยกแยะผู้สมัครที่มีประสบการณ์ production

Q: จะเกิดอะไรขึ้นเมื่อให้ service เดียวกันที่ทั้งระดับ module และ component?

Provider ระดับ component สร้าง instance แยกต่างหากที่มี scope ไปยัง subtree ของ component นั้น Service ที่ inject ใน subtree นั้นได้รับ instance ของ component ไม่ใช่ singleton ระดับ module สิ่งนี้เปิดใช้งานการแยก state เช่นเมื่อแต่ละ tab ต้องการ state form ของตัวเอง

Q: ทำไมต้องใช้ InjectionToken แทน string literal?

String token มีความเสี่ยงที่จะชนกันระหว่าง library หรือส่วนต่างๆ ของแอปพลิเคชัน InjectionToken สร้าง reference runtime ที่ไม่ซ้ำกันซึ่งไม่สามารถชนกันได้ พารามิเตอร์ type generic ยังให้ความปลอดภัยของ type ขณะ compile ที่ string token ขาด

Q: เมื่อไหร่ inject() throw เทียบกับ return undefined?

โดยค่าเริ่มต้น inject() throw เมื่อไม่พบ dependency การส่ง { optional: true } เปลี่ยน return type เป็น T | null และคืนค่า null แทนที่จะ throw สิ่งนี้ตรงกับพฤติกรรมของ decorator @Optional()

Q: อธิบายความแตกต่างระหว่าง providedIn: 'root' และการให้ใน array providers ของ module

ทั้งสองสร้าง singleton แต่ providedIn: 'root' เปิดใช้งาน tree-shaking Service จะรวมอยู่ใน bundle เฉพาะเมื่อ inject จริงที่ไหนสักที่ Provider ระดับ module จะรวมอยู่เสมอไม่ว่าจะใช้หรือไม่

Q: Lazy loading ส่งผลต่อ scope ของ service อย่างไร?

Lazy-loaded module ได้รับ child EnvironmentInjector ของตัวเอง Service ที่ให้ใน lazy module มี scope ไปยัง module นั้นและ children Service ที่มี providedIn: 'root' ยังคงเป็น singleton จริงในทุก module ไม่ว่าจะ lazy หรือไม่

สำหรับคำถามฝึกหัดเกี่ยวกับ Angular service และรูปแบบ DI ดูที่ คำถามสัมภาษณ์ Angular เกี่ยวกับ service และ dependency injection

รูปแบบปฏิบัติ: Configuration และ Feature Flag

แอปพลิเคชันจริงรวมแนวคิด DI เหล่านี้สำหรับการจัดการ configuration รูปแบบนี้ใช้ InjectionToken, factory provider และความตระหนักรู้ 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 นี้ render เนื้อหาตามเงื่อนไขตาม feature flag โดย configuration flag รวมศูนย์ในหนึ่ง injectable token

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แนวปฏิบัติ Angular DI สำหรับ Production

  • ใช้ providedIn: 'root' สำหรับ singleton ทั่วทั้งแอปพลิเคชันที่ได้ประโยชน์จาก tree-shaking
  • ใช้ inject() แทน constructor injection ใน Angular 20+ เพื่อ syntax ที่สะอาดกว่าและความเข้ากันได้กับ functional
  • กำหนด scope stateful service ไปยัง component เมื่อต้องการแยก ไม่ใช่ที่ระดับ module
  • สร้าง InjectionToken สำหรับ dependency non-class เพื่อให้แน่ใจว่า type-safe และหลีกเลี่ยงการชน
  • ใช้ @Optional() เมื่อ dependency อาจไม่มีอยู่ โดยเฉพาะสำหรับ plugin หรือ feature ที่เป็นตัวเลือก
  • ทดสอบ component ด้วย overridden provider โดยใช้ TestBed.overrideComponent() เพื่อการแยก
  • ใช้ multi-provider สำหรับรูปแบบการขยายเช่น validator, interceptor และ handler
ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Angular เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 13 กันยายน 2569

แท็ก

#angular
#dependency-injection
#typescript
#interview

แชร์

บทความที่เกี่ยวข้อง