Injection Tokens
Angular’s dependency injection uses a token as the key for every lookup, and most of the time that token is a class. But interfaces, primitives, and plain configuration objects have no class to act as a key — TypeScript interfaces vanish at runtime, and you can’t ask the injector for a string. The InjectionToken solves this: it creates a unique, type-safe key for any value, so configuration and constants flow through DI just like services do.
Why classes aren’t enough
A class doubles as both a token and a runtime value, which is why injecting a service “just works”. Non-class values break this assumption in two ways:
- Interfaces and types are erased during compilation, so there is nothing left at runtime to use as a key.
- Primitives and objects (a base URL, a feature flag, a config bag) aren’t unique — two different strings would collide on the same key.
InjectionToken gives you a single object that is guaranteed unique and carries a generic type parameter for compile-time safety.
Creating a token
You create a token once and export it. The generic type describes what the token resolves to, and the string argument is a human-readable description used only in error messages.
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
retryCount: number;
featureFlags: Record<string, boolean>;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export const API_URL = new InjectionToken<string>('api.url');
The token’s type travels with it everywhere — when you inject APP_CONFIG, Angular knows it’s an AppConfig, and when you provide it, TypeScript rejects a mismatched shape.
Providing a value
Because there’s no class to instantiate, you wire a token with an explicit provider recipe — most commonly useValue for static data or useFactory for computed values.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { APP_CONFIG, API_URL } from './app/tokens';
bootstrapApplication(AppComponent, {
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' },
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
retryCount: 3,
featureFlags: { darkMode: true },
},
},
],
});
Injecting a token
Inject a token with the same inject() function you use for services. The return type is inferred from the token’s generic, so autocomplete and type-checking work end to end.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { APP_CONFIG, API_URL } from './tokens';
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private config = inject(APP_CONFIG); // typed as AppConfig
private apiUrl = inject(API_URL); // typed as string
getUsers() {
console.log(`Calling ${this.apiUrl} (retries: ${this.config.retryCount})`);
return this.http.get(`${this.apiUrl}/users`);
}
}
Output:
Calling https://api.example.com (retries: 3)
Tree-shakable tokens with a default factory
Just as services use providedIn: 'root', a token can carry its own default factory. If nobody provides the token explicitly, Angular runs the factory — and if nobody injects it, the bundler drops it entirely.
export const RETRY_COUNT = new InjectionToken<number>('retry.count', {
providedIn: 'root',
factory: () => 3,
});
A factory can even inject other dependencies, letting one token derive its value from another:
export const HTTP_TIMEOUT = new InjectionToken<number>('http.timeout', {
providedIn: 'root',
factory: () => inject(APP_CONFIG).retryCount * 1000,
});
Tip: Prefer a token with a default
factoryover a separately registereduseValueprovider when there’s a sensible fallback — it keeps the token self-contained and tree-shakable, and you can still override it in tests or per feature.
Multi-value tokens
Setting multi: true makes a token collect all contributions into an array instead of overwriting. This is how Angular’s own HTTP_INTERCEPTORS works, and it’s perfect for plugin-style registration.
export const VALIDATORS = new InjectionToken<((v: string) => boolean)[]>('validators');
const providers = [
{ provide: VALIDATORS, useValue: (v: string) => v.length > 0, multi: true },
{ provide: VALIDATORS, useValue: (v: string) => v.includes('@'), multi: true },
];
// inject(VALIDATORS) resolves to an array of both functions
Recipe reference
| Recipe | Use it when |
|---|---|
useValue | The value is a known constant or pre-built object. |
useFactory | The value must be computed, possibly from other injected deps. |
factory (on the token) | You want a tree-shakable default with no separate provider. |
multi: true | Many providers should contribute entries to one array. |
Warning: A token is identified by object reference, not by its description string. Always export and import the same
InjectionTokeninstance — re-creating an identically-named token in another file produces a different key and aNullInjectorError.
Best practices
- Reach for
InjectionTokenfor every non-class dependency: config objects, base URLs, feature flags, and function values. - Always pass the generic type parameter so injection sites stay fully typed.
- Give tokens a descriptive string (e.g.
'app.config') to makeNullInjectorErrormessages readable. - Use a default
factorywithprovidedIn: 'root'when a sensible fallback exists, keeping the token tree-shakable. - Use
multi: truefor extensible, plugin-style lists rather than injecting and merging arrays by hand. - Define tokens in a dedicated
tokens.tsand import that single instance everywhere to avoid duplicate-token bugs.