All topics
Servicesintermediate

Injection Tokens

Explain why InjectionToken exists and how it enables injecting non-class values like config objects or primitives.

Angular's DI system normally uses a class itself as the lookup key for a provider (inject HttpClient, get an HttpClient instance), but TypeScript interfaces, type aliases, and plain values (strings, objects, functions) don't exist at runtime — they're erased during compilation — so there's no runtime identity to use as a DI lookup key for them. InjectionToken<T> solves this by creating a unique, runtime-existing object specifically to serve as that lookup key, while still carrying the type T for compile-time type safety wherever it's injected.

It's like a coat-check ticket stub for a coat that has no owner's name sewn into the lining — the stub itself (the token) is the only reliable way to retrieve the right coat later, since the coat alone carries no identifying information.

Key Concepts

1
This comes up constantly for app-wide configuration objects — API base URLs, feature flags, environment-specific settings — where you want the value to be injectable (so it can be swapped in tests, or vary between environments) but there's no natural class to represent "a string" or "a plain config object."
2
A well-designed InjectionToken includes a description string (useful for debugging DI errors, since it shows up in error messages) and often a providedIn + factory combination directly on the token itself, which lets the token be self-contained and tree-shakable, the same way providedIn: 'root' works for @Injectable() classes.
InjectionTokenprovidedInfactoryprovidedIn: 'root'@Injectable()
3
Interviewers sometimes ask you to inject a token using the @Inject() decorator (constructor(@Inject(API_CONFIG) private config: ApiConfig)) versus the newer inject(API_CONFIG) function call — both work, but @Inject() is required specifically because TypeScript's parameter decorator metadata can't capture a token that isn't itself a class or interface with runtime identity, whereas inject() sidesteps that entirely since it's a plain function call, not decorator metadata.
@Inject()constructor(@Inject(API_CONFIG) private config: ApiConfig)inject(API_CONFIG)inject()