Skip to content

Commit df246bb

Browse files
committed
feat(core): options object to supersede bit flags for inject()
`inject()` originated as a private API and was made public to support `InjectionToken` factories in Ivy. For code-size and performance reasons, when we code generate `inject()` calls we use a bit field to indicate the various injection modes (optional, skip-self, etc). However, this doesn't make for a very nice public API. This commit introduces an alternative object-based API for options. All 4 flags are supported as `boolean` fields on an options object, and converted to bit flags internally. If TypeScript can prove that `optional` injection is not requested, it can narrow the return type and remove the `null` type. DEPRECATED: The bit field signature of `inject()` has been deprecated, in favor of the new options object. Correspondingly, `InjectFlags` is deprecated as well. Fixes #46251
1 parent 6f11a58 commit df246bb

File tree

5 files changed

+183
-5
lines changed

5 files changed

+183
-5
lines changed

goldens/public-api/core/index.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -600,9 +600,17 @@ export const Inject: InjectDecorator;
600600
// @public (undocumented)
601601
export function inject<T>(token: ProviderToken<T>): T;
602602

603-
// @public (undocumented)
603+
// @public @deprecated (undocumented)
604604
export function inject<T>(token: ProviderToken<T>, flags?: InjectFlags): T | null;
605605

606+
// @public (undocumented)
607+
export function inject<T>(token: ProviderToken<T>, options: InjectOptions & {
608+
optional?: false;
609+
}): T;
610+
611+
// @public (undocumented)
612+
export function inject<T>(token: ProviderToken<T>, options: InjectOptions): T | null;
613+
606614
// @public
607615
export interface Injectable {
608616
providedIn?: Type<any> | 'root' | 'platform' | 'any' | null;
@@ -641,7 +649,7 @@ export interface InjectDecorator {
641649
new (token: any): Inject;
642650
}
643651

644-
// @public
652+
// @public @deprecated
645653
export enum InjectFlags {
646654
Default = 0,
647655
Host = 1,
@@ -664,6 +672,14 @@ export class InjectionToken<T> {
664672
readonly ɵprov: unknown;
665673
}
666674

675+
// @public
676+
export interface InjectOptions {
677+
host?: boolean;
678+
optional?: boolean;
679+
self?: boolean;
680+
skipSelf?: boolean;
681+
}
682+
667683
// @public
668684
export const INJECTOR: InjectionToken<Injector>;
669685

packages/core/src/di/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export {EnvironmentInjector} from './r3_injector';
2222
export {importProvidersFrom, ImportProvidersSource} from './provider_collection';
2323
export {ENVIRONMENT_INITIALIZER} from './initializer_token';
2424
export {ProviderToken} from './provider_token';
25-
export {ɵɵinject, inject, ɵɵinvalidFactoryDep} from './injector_compatibility';
25+
export {ɵɵinject, inject, InjectOptions, ɵɵinvalidFactoryDep} from './injector_compatibility';
2626
export {INJECTOR} from './injector_token';
2727
export {ReflectiveInjector} from './reflective_injector';
2828
export {ClassProvider, ModuleWithProviders, ClassSansProvider, ImportedNgModuleProviders, ConstructorProvider, ConstructorSansProvider, ExistingProvider, ExistingSansProvider, FactoryProvider, FactorySansProvider, Provider, StaticClassProvider, StaticClassSansProvider, StaticProvider, TypeProvider, ValueProvider, ValueSansProvider} from './interface/provider';

packages/core/src/di/injector_compatibility.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,35 @@ Please check that 1) the type for the parameter at index ${
102102
index} is correct and 2) the correct Angular decorators are defined for this class and its ancestors.`);
103103
}
104104

105+
/**
106+
* Type of the options argument to `inject`.
107+
*
108+
* @publicApi
109+
*/
110+
export interface InjectOptions {
111+
/**
112+
* Use optional injection, and return `null` if the requested token is not found.
113+
*/
114+
optional?: boolean;
115+
116+
/**
117+
* Start injection at the parent of the current injector.
118+
*/
119+
skipSelf?: boolean;
120+
121+
/**
122+
* Only query the current injector for the token, and don't fall back to the parent injector if
123+
* it's not found.
124+
*/
125+
self?: boolean;
126+
127+
/**
128+
* Stop injection at the host component's injector. Only relevant when injecting from an element
129+
* injector, and a no-op for environment injectors.
130+
*/
131+
host?: boolean;
132+
}
133+
105134
/**
106135
* @param token A token that represents a dependency that should be injected.
107136
* @returns the injected value if operation is successful, `null` otherwise.
@@ -118,8 +147,33 @@ export function inject<T>(token: ProviderToken<T>): T;
118147
* @throws if called outside of a supported context.
119148
*
120149
* @publicApi
150+
* @deprecated prefer an options object instead of `InjectFlags`
121151
*/
122152
export function inject<T>(token: ProviderToken<T>, flags?: InjectFlags): T|null;
153+
/**
154+
* @param token A token that represents a dependency that should be injected.
155+
* @param options Control how injection is executed. Options correspond to injection strategies
156+
* that can be specified with parameter decorators `@Host`, `@Self`, `@SkipSelf`, and
157+
* `@Optional`.
158+
* @returns the injected value if operation is successful.
159+
* @throws if called outside of a supported context, or if the token is not found.
160+
*
161+
* @publicApi
162+
*/
163+
export function inject<T>(token: ProviderToken<T>, options: InjectOptions&{optional?: false}): T;
164+
/**
165+
* @param token A token that represents a dependency that should be injected.
166+
* @param options Control how injection is executed. Options correspond to injection strategies
167+
* that can be specified with parameter decorators `@Host`, `@Self`, `@SkipSelf`, and
168+
* `@Optional`.
169+
* @returns the injected value if operation is successful, `null` if the token is not
170+
* found and optional injection has been requested.
171+
* @throws if called outside of a supported context, or if the token is not found and optional
172+
* injection was not requested.
173+
*
174+
* @publicApi
175+
*/
176+
export function inject<T>(token: ProviderToken<T>, options: InjectOptions): T|null;
123177
/**
124178
* Injects a token from the currently active injector.
125179
* `inject` is only supported during instantiation of a dependency by the DI system. It can be used
@@ -184,7 +238,18 @@ export function inject<T>(token: ProviderToken<T>, flags?: InjectFlags): T|null;
184238
*
185239
* @publicApi
186240
*/
187-
export function inject<T>(token: ProviderToken<T>, flags = InjectFlags.Default): T|null {
241+
export function inject<T>(
242+
token: ProviderToken<T>, flags: InjectFlags|InjectOptions = InjectFlags.Default): T|null {
243+
if (typeof flags !== 'number') {
244+
// While TypeScript doesn't accept it without a cast, bitwise OR with false-y values in
245+
// JavaScript is a no-op. We can use that for a very codesize-efficient conversion from
246+
// `InjectOptions` to `InjectFlags`.
247+
flags = (InternalInjectFlags.Default | // comment to force a line break in the formatter
248+
((flags.optional && InternalInjectFlags.Optional) as number) |
249+
((flags.host && InternalInjectFlags.Host) as number) |
250+
((flags.self && InternalInjectFlags.Self) as number) |
251+
((flags.skipSelf && InternalInjectFlags.SkipSelf) as number)) as InjectFlags;
252+
}
188253
return ɵɵinject(token, flags);
189254
}
190255

packages/core/src/di/interface/injector.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const enum DecoratorFlags {
2020
* Injection flags for DI.
2121
*
2222
* @publicApi
23+
* @deprecated use an options object for `inject` instead.
2324
*/
2425
export enum InjectFlags {
2526
// TODO(alxhub): make this 'const' (and remove `InternalInjectFlags` enum) when ngc no longer

packages/core/test/acceptance/di_spec.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {CommonModule} from '@angular/common';
10-
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE} from '@angular/core';
10+
import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE} from '@angular/core';
1111
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
1212
import {TestBed} from '@angular/core/testing';
1313
import {By} from '@angular/platform-browser';
@@ -3328,6 +3328,102 @@ describe('di', () => {
33283328
fixture.detectChanges();
33293329
expect(fixture.nativeElement.innerHTML).toEqual('default value');
33303330
});
3331+
3332+
describe('with an options object argument', () => {
3333+
it('should be able to optionally inject a service', () => {
3334+
const TOKEN = new InjectionToken<string>('TOKEN');
3335+
3336+
@Component({
3337+
standalone: true,
3338+
template: '',
3339+
})
3340+
class TestCmp {
3341+
value = inject(TOKEN, {optional: true});
3342+
}
3343+
3344+
expect(TestBed.createComponent(TestCmp).componentInstance.value).toBeNull();
3345+
});
3346+
3347+
it('should be able to use skipSelf injection', () => {
3348+
const TOKEN = new InjectionToken<string>('TOKEN', {
3349+
providedIn: 'root',
3350+
factory: () => 'from root',
3351+
});
3352+
@Component({
3353+
standalone: true,
3354+
template: '',
3355+
providers: [{provide: TOKEN, useValue: 'from component'}],
3356+
})
3357+
class TestCmp {
3358+
value = inject(TOKEN, {skipSelf: true});
3359+
}
3360+
3361+
expect(TestBed.createComponent(TestCmp).componentInstance.value).toEqual('from root');
3362+
});
3363+
3364+
it('should be able to use self injection', () => {
3365+
const TOKEN = new InjectionToken<string>('TOKEN', {
3366+
providedIn: 'root',
3367+
factory: () => 'from root',
3368+
});
3369+
3370+
@Component({
3371+
standalone: true,
3372+
template: '',
3373+
})
3374+
class TestCmp {
3375+
value = inject(TOKEN, {self: true, optional: true});
3376+
}
3377+
3378+
expect(TestBed.createComponent(TestCmp).componentInstance.value).toBeNull();
3379+
});
3380+
3381+
it('should be able to use host injection', () => {
3382+
const TOKEN = new InjectionToken<string>('TOKEN');
3383+
3384+
@Component({
3385+
standalone: true,
3386+
selector: 'child',
3387+
template: '{{value}}',
3388+
})
3389+
class ChildCmp {
3390+
value = inject(TOKEN, {host: true, optional: true}) ?? 'not found';
3391+
}
3392+
3393+
@Component({
3394+
standalone: true,
3395+
imports: [ChildCmp],
3396+
template: '<child></child>',
3397+
providers: [{provide: TOKEN, useValue: 'from parent'}],
3398+
encapsulation: ViewEncapsulation.None,
3399+
})
3400+
class ParentCmp {
3401+
}
3402+
3403+
const fixture = TestBed.createComponent(ParentCmp);
3404+
fixture.detectChanges();
3405+
expect(fixture.nativeElement.innerHTML).toEqual('<child>not found</child>');
3406+
});
3407+
3408+
it('should not indicate it returns null when optional is explicitly false', () => {
3409+
const TOKEN = new InjectionToken<string>('TOKEN', {
3410+
providedIn: 'root',
3411+
factory: () => 'from root',
3412+
});
3413+
3414+
@Component({
3415+
standalone: true,
3416+
template: '',
3417+
})
3418+
class TestCmp {
3419+
// TypeScript will check if this assignment is legal, which won't be the case if
3420+
// inject() erroneously returns a `string|null` type here.
3421+
value: string = inject(TOKEN, {optional: false});
3422+
}
3423+
3424+
expect(TestBed.createComponent(TestCmp).componentInstance.value).toEqual('from root');
3425+
});
3426+
});
33313427
});
33323428

33333429
it('should be able to use Host in `useFactory` dependency config', () => {

0 commit comments

Comments
 (0)