Dependency Injection (DI) is a pattern where dependencies are supplied to a class instead of created inside it. Angular has a built-in DI system that handles creation and sharing of services.
1. Creating a Service
Decorate a class with @Injectable to make it injectable. providedIn: 'root' registers it as an app-wide singleton that is also tree-shakable — if nothing injects it, it won't be in the bundle:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
isLoggedIn = false;
login() { this.isLoggedIn = true; }
logout() { this.isLoggedIn = false; }
}
2. @Service() — Angular 22+
Angular 22 introduced @Service() as a shorthand for @Injectable({ providedIn: 'root' }). Less boilerplate, same behavior:
import { Service } from '@angular/core';
@Service()
export class AuthService {
isLoggedIn = false;
login() { this.isLoggedIn = true; }
logout() { this.isLoggedIn = false; }
}
Note:
@Service()only works withinject()— it does not support constructor injection. For most new services, prefer@Service()when targeting Angular 22+.
3. Injecting a Service
Use the inject() function in a class field — this is the modern, preferred approach:
import { Component, inject } from '@angular/core';
import { AuthService } from './auth.service';
@Component({ ... })
export class NavbarComponent {
private auth = inject(AuthService);
logout() { this.auth.logout(); }
}
Constructor injection is equivalent and still valid, but inject() is cleaner:
constructor(private auth: AuthService) {}
4. Providing at Component Level
Adding a service to a component's providers array creates a new instance scoped to that component and its children. The root singleton is untouched.
@Component({
selector: 'app-cart',
providers: [CartService],
template: `...`,
})
export class CartComponent {
private cart = inject(CartService);
}
Useful when different parts of the UI need isolated state (e.g. two independent forms on the same page).
5. InjectionToken
For non-class values like config objects, strings, or primitives, use InjectionToken. TypeScript interfaces are erased at runtime and cannot be used as tokens.
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
production: boolean;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
Provide it where you bootstrap the app:
bootstrapApplication(AppComponent, {
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: 'https://api.example.com', production: false } }
]
});
Inject it normally:
private config = inject(APP_CONFIG);
6. Provider Types
useValue — static value
The most common use is injecting configuration:
providers: [
{ provide: APP_CONFIG, useValue: { apiUrl: '...', production: false } }
]
useClass — substitute an implementation
Swap one class for another — handy for mocking in tests:
providers: [
{ provide: Logger, useClass: MockLogger }
]
useFactory — compute the value at runtime
When the value depends on other services or runtime conditions:
providers: [
{
provide: LoggerService,
useFactory: () => {
const config = inject(APP_CONFIG);
return new LoggerService(config.production ? 'error' : 'debug');
}
}
]
7. Optional Injection
If a dependency may or may not be provided, use { optional: true } to get null instead of an error:
private analytics = inject(AnalyticsService, { optional: true });
ngOnInit() {
this.analytics?.track('page_view');
}
Top comments (0)