Recently, Angular put in place a migration to replace constructor-based injection with inject
function.
In most of the cases, it is straightforward and it clearly has benefits:
// before
@Injectable({ providedIn: 'root' })
export class SomeSearchService {
constructor(
@Inject(SEARCH_DUE_TIME)
private readonly dueTime: number,
private readonly repositoryService: SomeRepositoryService
) {}
// after: more concise, better typing
@Injectable({ providedIn: 'root' })
export class SomeSearchService {
private readonly dueTime = inject(SEARCH_DUE_TIME);
private readonly repositoryService= inject(SomeRepositoryService);
However, can inject
function cover all the constructor-based injection usages?
In particular with useFactory
+ deps
?
To illustrate, I have this Stackblitz:
// print.service.ts
@Injectable()
export class PrintService implements IPrintService {
constructor( // QUESTION: can I replace it with `inject` calls?
@Inject(DATA_SERVICE)
private readonly dataService: IDataService,
private readonly logger: LoggingService,
...
) {}
// tokens.ts
export const DATA_SERVICE = new InjectionToken<IDataService>('DATA_SERVICE');
export const FOO_PRINT_SERVICE = new InjectionToken<IPrintService>('FOO_PRINT_SERVICE');
export const BAR_PRINT_SERVICE = new InjectionToken<IPrintService>('BAR_PRINT_SERVICE');
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{
provide: FOO_PRINT_SERVICE,
useFactory: (dataService: IDataService, logger: LoggingService) => new PrintService(dataService, logger),
deps: [FOO_DATA_SERVICE, LoggingService],
},
{
provide: BAR_PRINT_SERVICE ,
useFactory: (dataService: IDataService, logger: LoggingService) => new PrintService(dataService, logger),
deps: [BAR_DATA_SERVICE, LoggingService],
},
...,
],
};
Is there a way to use inject
& useFactory
+ deps
together
so that I can replace the constructor
declaration in my PrintService
with inject
calls?
Most of the constructor-based injections are straightforwardly replaceable with the inject
function:
// before
export class MyService {
constructor(
@Host() private readonly fooService: FooService,
@Optional() private readonly barService: BarService,
@Self() private readonly bazService: BazService,
@SkipSelf() private readonly quxService: QuxService,
@Inject(SOME_TOKEN) private readonly someToken: string,
private readonly otherService: OtherService
) {}
// after
@Injectable()
export class MyService {
private readonly fooService = inject(FooService, { host: true });
private readonly barService = inject(BarService, { optional: true });
private readonly bazService = inject(BazService, { self: true });
private readonly quxService = inject(QuxService, { skipSelf: true });
private readonly someToken = inject(SOME_TOKEN);
private readonly otherService = inject(OtherService);
As mentioned previously, there is even a migration to change them automatically.
The usecase of this question with useFactory
+ deps
is a bit more tricky.
The problematic is to create several instances of the same service at the same injection level
(i.e.: ApplicationConfig.providers
), but with different dependencies.
In other words, we would need sort of injection scopes where inject(DATA_SERVICE)
would point
either to the FOO_DATA_SERVICE
or to the BAR_DATA_SERVICE
provided instance.
To do so, it is possible to rely on Injector.create
:
it returns a brand new independent injectors with specific providers
// print.service.ts
@Injectable()
export class PrintService implements IPrintService {
private readonly dataService = inject(DATA_SERVICE);
private readonly logger = inject(LoggingService);
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
...,
{
provide: FOO_PRINT_SERVICE,
useFactory: (environmentInjector: EnvironmentInjector) => {
const injector = Injector.create({
providers: [
{ provide: DATA_SERVICE, useValue: { getData: () => of('foo') } },
PrintService,
],
parent: environmentInjector,
});
return injector.get(PrintService);
},
deps: [EnvironmentInjector],
},
{
provide: BAR_PRINT_SERVICE,
useFactory: (environmentInjector: EnvironmentInjector) => {
const injector = Injector.create({
providers: [
{ provide: DATA_SERVICE, useValue: { getData: () => of('bar') } },
PrintService,
],
parent: environmentInjector,
});
return injector.get(PrintService);
},
deps: [EnvironmentInjector],
},
],
};
In this example, I was able to delete the FOO_DATA_SERVICE
& BAR_DATA_SERVICE
intermediate tokens.
Note that the use of a parent
injector (here EnvironmentInjector
) is optional and depends on your needs.
Finally, you may want to recycle some intermediate services when using such "scoped" injectors.
A technique for that can be to store those injectors in intermediate tokens:
// common-repository.providers.ts
export function provideCommonRepositoryFeature(payload: { ... }) {
const injectorToken = new InjectionToken<Injector>(`COMMON_REPOSITORY_INJECTOR_${getUniqueId()}`);
return makeEnvironmentProviders([
{
provide: injectorToken,
useFactory: (environmentInjector: EnvironmentInjector) =>
Injector.create({
providers: [
{
provide: COMMON_REPOSITORY_CONFIGURATION_FACADE, // Provide module's specificities
useValue: payload.configFacade,
},
CommonRepositoryApiEndpointService, // => this service will be recycled when providing the 2 target tokens below
CommonRepositoryResultsApiMapperRepositoryService,
CommonRepositoryResultsMockRepositoryService,
],
parent: environmentInjector,
}),
deps: [EnvironmentInjector],
},
{
provide: payload.targetApiMapperRepositoryToken,
useFactory: (injector: Injector) => injector.get(CommonRepositoryResultsApiMapperRepositoryService),
deps: [injectorToken],
},
{
provide: payload.targetMockRepositoryToken,
useFactory: (injector: Injector) => injector.get(CommonRepositoryResultsMockRepositoryService),
deps: [injectorToken],
},
]);
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideCommonRepositoryFeature({
configFacade: { modulePath: 'foo' },
targetApiMapperRepositoryToken: FOO_API_MAPPER_REPOSITORY,
targetMockRepositoryToken: FOO_MOCK_REPOSITORY,
}),
provideCommonRepositoryFeature({
configFacade: { modulePath: 'bar' },
targetApiMapperRepositoryToken: BAR_API_MAPPER_REPOSITORY,
targetMockRepositoryToken: BAR_MOCK_REPOSITORY,
}),
],
};