Injection Tokens
Angular's dependency injection works great with class types — you inject UserService and Angular knows what to create. But what if you want to inject a plain string, number, interface, or configuration object? Classes can't describe those.
Injection Tokens solve this problem. They are special objects that serve as unique keys in the DI system, allowing you to provide and inject any value — not just class instances.
The Problem Without Tokens
// TypeScript interfaces are erased at runtime — you can't use them as DI tokens
export interface ApiConfig {
baseUrl: string;
timeout: number;
}
// This will NOT work — interfaces don't exist at runtime
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private config: ApiConfig) {} // ERROR: No provider for ApiConfig
}Creating an InjectionToken
import { InjectionToken } from '@angular/core';
// Generic type parameter makes it fully type-safe
export const API_CONFIG = new InjectionToken<ApiConfig>('ApiConfig');
// The string is a description for debugging — it shows up in error messages
// It does NOT have to be unique, but best practice is to make it descriptiveCommon Use Cases
1. Application Configuration
// tokens.ts — define all tokens in one place
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
appName: string;
maxUploadSizeMb: number;
supportedLocales: string[];
}
export const APP_CONFIG = new InjectionToken<AppConfig>('AppConfig');
export const API_URL = new InjectionToken<string>('ApiUrl');
export const MAX_RETRY = new InjectionToken<number>('MaxRetry');// main.ts — provide values at bootstrap
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { APP_CONFIG, API_URL, MAX_RETRY } from './app/tokens';
bootstrapApplication(AppComponent, {
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.myapp.com',
appName: 'MyApp',
maxUploadSizeMb: 50,
supportedLocales: ['en', 'de', 'fr'],
},
},
{ provide: API_URL, useValue: 'https://api.myapp.com' },
{ provide: MAX_RETRY, useValue: 3 },
],
});// Inject using inject() function
import { inject, Injectable } from '@angular/core';
import { APP_CONFIG, API_URL } from './tokens';
@Injectable({ providedIn: 'root' })
export class HttpService {
private apiUrl = inject(API_URL);
private config = inject(APP_CONFIG);
uploadFile(file: File): Observable<void> {
if (file.size > this.config.maxUploadSizeMb * 1024 * 1024) {
throw new Error(`File exceeds ${this.config.maxUploadSizeMb}MB limit`);
}
return this.http.post<void>(`${this.apiUrl}/upload`, file);
}
}2. Feature Flags
export interface FeatureFlags {
darkMode: boolean;
betaAnalytics: boolean;
aiAssistant: boolean;
}
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FeatureFlags');
// In app config
providers: [
{
provide: FEATURE_FLAGS,
useValue: {
darkMode: true,
betaAnalytics: false,
aiAssistant: environment.production ? false : true,
},
},
]
// In a component
@Component({ selector: 'app-header' })
export class HeaderComponent {
private flags = inject(FEATURE_FLAGS);
showAiButton = this.flags.aiAssistant;
}3. Tokens with Default Values
You can provide a default factory directly on the token itself using the second argument. This means the token works even without an explicit provider:
export const THEME = new InjectionToken<'light' | 'dark'>('Theme', {
providedIn: 'root',
factory: () => {
// Default: detect from OS preference
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
},
});
// No provider needed — the factory runs automatically
@Component({ selector: 'app-root' })
export class AppComponent {
theme = inject(THEME); // 'light' or 'dark' from OS preference
}4. Injecting Functions
// Inject a logging function rather than a whole service
type LogFn = (message: string, level?: 'info' | 'warn' | 'error') => void;
export const LOGGER = new InjectionToken<LogFn>('Logger', {
providedIn: 'root',
factory: () => (message: string, level = 'info') => {
console[level](`[${level.toUpperCase()}] ${message}`);
},
});
// Usage
@Injectable({ providedIn: 'root' })
export class OrderService {
private log = inject(LOGGER);
placeOrder(order: Order) {
this.log(`Placing order ${order.id}`, 'info');
}
}5. Platform and Environment Tokens
import { DOCUMENT, PLATFORM_ID, isPlatformBrowser } from '@angular/common';
import { inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class BrowserService {
private platformId = inject(PLATFORM_ID);
private document = inject(DOCUMENT);
isBrowser(): boolean {
return isPlatformBrowser(this.platformId);
}
getTitle(): string {
return this.isBrowser() ? this.document.title : '';
}
}Built-in Angular tokens you can inject:
Token | Package | Provides |
|---|---|---|
DOCUMENT | @angular/common | The DOM document object |
PLATFORM_ID | @angular/core | String: 'browser', 'server', or 'worker' |
APP_ID | @angular/core | Application ID string |
LOCALE_ID | @angular/core | Current locale string (e.g. "en-US") |
ENVIRONMENT_INITIALIZER | @angular/core | Runs initialization code |
APP_INITIALIZER | @angular/core | Runs before app renders |
BASE_HREF | @angular/common | The base href from index.html |
Using @Inject Decorator
When using constructor injection (rather than inject()), use the @Inject decorator to specify the token:
import { Inject, Injectable } from '@angular/core';
import { API_URL, MAX_RETRY } from './tokens';
@Injectable({ providedIn: 'root' })
export class RetryHttpService {
constructor(
@Inject(API_URL) private apiUrl: string,
@Inject(MAX_RETRY) private maxRetry: number,
private http: HttpClient,
) {}
}Tokens in Testing
Injection tokens make testing straightforward — override any configuration value without touching production code:
describe('HttpService', () => {
let service: HttpService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
HttpService,
// Override the API URL for tests
{ provide: API_URL, useValue: 'http://localhost:3000' },
{ provide: MAX_RETRY, useValue: 1 },
{
provide: FEATURE_FLAGS,
useValue: { darkMode: false, betaAnalytics: true, aiAssistant: false },
},
],
});
service = TestBed.inject(HttpService);
});
it('should use the test API URL', () => {
// service uses localhost:3000 in tests
});
});Best Practices
Always use InjectionToken for non-class values (strings, numbers, interfaces, objects)
Define all tokens in a single tokens.ts file at the feature or app level for discoverability
Give tokens descriptive string descriptions — they appear in error messages and DevTools
Use the generic type parameter to get compile-time type safety: new InjectionToken<MyInterface>(...)
Prefer token factories (second argument) for tokens with sensible defaults — avoids mandatory providers
Use @Optional with inject() when a token might not be provided: inject(TOKEN, { optional: true })
Summary
InjectionToken unlocks the full power of Angular's DI system for non-class values:
- Create with
new InjectionToken<T>('description') - Provide with
{ provide: MY_TOKEN, useValue: ... }or factory - Inject with
inject(MY_TOKEN)or@Inject(MY_TOKEN)in constructors - Supports default factory, optional injection, and multi-providers
- Perfect for config objects, feature flags, environment values, and platform APIs
With tokens mastered, you have the full DI toolbox at your disposal.