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

ระบบ 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
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
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
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
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
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); }
}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 ตามเส้นทางนี้:
- ตรวจสอบ ElementInjector ของ component ที่ร้องขอ
- เดินขึ้นต้นไม้ ElementInjector ไปยัง ancestor
- ตรวจสอบ EnvironmentInjector (module หรือ standalone provider)
- เดินขึ้นไปยัง root EnvironmentInjector
- ถึง NullInjector และ throw error ถ้าไม่ได้ใช้
@Optional()
Modifier การ Resolve: @Self, @SkipSelf, @Optional, @Host
Modifier การ resolve เปลี่ยนวิธีที่ Angular ค้นหาลำดับชั้น injector Decorator เหล่านี้ทำงานกับทั้ง constructor injection และฟังก์ชัน inject()
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 แต่ไม่ควรเอื้อมถึงที่สูงกว่าในต้นไม้
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 และรูปแบบการขยาย
import { InjectionToken } from '@angular/core';
export interface Validator {
validate(value: string): string | null;
}
export const VALIDATORS = new InjectionToken<Validator[]>('validators');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 }
]
};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
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 };
}
});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ผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 13 กันยายน 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

ไวยากรณ์ Control Flow ใน Angular 2026: @if, @for, @switch และคำถามสัมภาษณ์
เรียนรู้ไวยากรณ์ control flow ของ Angular (@if, @for, @switch) ที่มาแทนที่ structural directive ครบถ้วนพร้อมตัวอย่างโค้ด เคล็ดลับการ migrate และคำถามสัมภาษณ์เทคนิค

Angular Signals และ Computed ในปี 2026: Fine-Grained Reactivity และคำถามสัมภาษณ์
เรียนรู้ Angular Signals และ computed signals สำหรับ fine-grained reactivity ใน Angular 20+ คู่มือครบถ้วนครอบคลุม signal primitives, linkedSignal, การทำงานร่วมกับ RxJS และคำถามสัมภาษณ์ทางเทคนิค

Angular HttpClient และ Interceptor ในปี 2026: การจัดการ Request และคำถามสัมภาษณ์
เรียนรู้วิธีใช้ Angular HttpClient และ functional interceptor สำหรับการจัดการ HTTP request, การยืนยันตัวตนด้วย token, retry logic และ caching พร้อมคำถามสัมภาษณ์ Angular ที่พบบ่อย