The @Injectable Decorator and Providers
The @Injectable() decorator does two jobs: it marks a class as injectable (so Angular emits the metadata needed to instantiate it and resolve its own dependencies), and optionally it registers the class with an injector via its providedIn option. But Angular's DI system is more flexible than 'inject a class and get an instance of that same class' — providers let you control exactly what value an injection token resolves to, decoupling what code asks for from what it actually receives.
Cricket analogy: @Injectable() marking a class as instantiable is like a player being formally registered with the board so selectors can call them up, while providedIn is like being pre-assigned to a specific franchise squad rather than a free agent picked ad hoc.
Provider types
Angular supports four main provider shapes. A useClass provider maps a token to a different class than the token itself, useful for swapping implementations (e.g. a mock service in tests). A useValue provider supplies a static, already-constructed value — ideal for configuration objects or constants. A useFactory provider runs a function to compute the value at injection time, optionally with its own dependencies via deps. A useExisting provider makes one token resolve to the same instance as another existing token, useful for aliasing.
Cricket analogy: useClass, useValue, useFactory, and useExisting are like swapping a specialist spinner in for a pacer on a turning pitch (useClass), fielding a fixed pre-set batting order (useValue), calculating the XI based on pitch conditions at the toss (useFactory), and naming a vice-captain who's really just the same person as the deputy (useExisting).
import { InjectionToken, Injectable } from '@angular/core';
export interface AppConfig { apiUrl: string; }
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
@Injectable({ providedIn: 'root' })
export class LoggerService {
log(msg: string) { console.log(msg); }
}
@Injectable({ providedIn: 'root' })
export class ConsoleLoggerService extends LoggerService {}
// In app.config.ts (application-level providers):
export const appConfigProviders = [
{ provide: APP_CONFIG, useValue: { apiUrl: 'https://api.example.com' } },
{ provide: LoggerService, useClass: ConsoleLoggerService },
{
provide: 'RETRY_COUNT',
useFactory: (config: AppConfig) => (config.apiUrl.includes('staging') ? 5 : 3),
deps: [APP_CONFIG],
},
];InjectionToken for non-class values
Because plain interfaces and primitive types don't exist at runtime (TypeScript types are erased during compilation), you can't use them directly as DI tokens. InjectionToken<T> creates a unique, type-safe runtime token you can provide and inject in place of a class, commonly used for configuration objects, feature flags, or any dependency that isn't naturally a class instance.
Cricket analogy: InjectionToken is like the ICC creating a formal 'Player of the Match' award category that doesn't correspond to any single physical trophy class, giving a unique, recognized token you can present even though 'Player of the Match' isn't a tangible object type.
This pattern is conceptually similar to dependency injection containers in other ecosystems, such as .NET's IServiceCollection or Java Spring's @Bean definitions: you separate 'the contract something depends on' (the token) from 'what concretely satisfies that contract' (the provider), which is what makes swapping real implementations for test doubles straightforward without touching consuming code.
A frequent mistake is providing the same token at multiple levels (e.g. both providedIn: 'root' and again in a component's providers array) without realizing the component-level provider creates an entirely separate instance for that component's injector subtree, shadowing the root singleton for anything injected under that component. This is sometimes intentional (scoping a service to a feature) but often an accidental source of 'why isn't my state shared' bugs.
Providers can be supplied at the root level via providedIn or an application-wide providers array in app.config.ts, or at a narrower level via a component's or route's providers array, which determines the scope and lifetime of the instance created — a topic explored in depth alongside the injector hierarchy.
Cricket analogy: Providers at root versus component/route level are like a franchise having a central head coach available to the whole squad (root providedIn) versus a specialist fielding coach brought in just for one fielding drill session (component-level provider), scoping their involvement and lifetime accordingly.
- @Injectable() marks a class for DI and can register it with an injector via providedIn.
- useClass swaps in a different implementation class for a given token.
- useValue supplies a static, pre-built value, commonly for configuration objects.
- useFactory computes the provided value at injection time, optionally using its own deps.
- useExisting aliases one token to resolve to the same instance as another.
- InjectionToken<T> creates a type-safe runtime token for values that aren't classes, like interfaces or config objects.
Practice what you learned
1. What is the purpose of a useFactory provider?
2. Why would you use an InjectionToken<T> instead of a class as a DI token?
3. What does a useExisting provider do?
4. What happens if the same token is provided both at the root level and again in a component's providers array?
5. Which provider configuration is most appropriate for supplying a static configuration object like { apiUrl: string }?
Was this page helpful?
You May Also Like
Creating and Injecting Services
Learn how to define an Angular service class, register it with Angular's dependency injection system, and inject it into components and other services.
Hierarchical Dependency Injection
Understand how Angular's injectors form a tree mirroring the component hierarchy, how token resolution walks up that tree, and how to scope service instances deliberately.
The inject() Function
Learn how the inject() function offers a flexible alternative to constructor injection, and how it enables DI in functional contexts like guards, resolvers, and interceptors.
Unit Testing with Jasmine and Karma
Learn how Angular's default testing stack—Jasmine for specs and Karma for running them in a real browser—verifies component logic, services, and pipes in isolation.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics