A constructor parameter typed as a class unioned with null (MyService | null) fails token resolution. OXC emits ɵɵinvalidFactoryDep, which throws at instantiation. This is a token-resolution bug — independent of optionality.
Repro (both fail)
// canonical optional-DI pattern — still broken
@Component({ selector: 'x', template: '' })
export class X { constructor(@Optional() private svc: MyService | null) {} }
// no decorator — also broken
export class Y { constructor(private svc: MyService | null) {} }
Actual
ɵfac = function X_Factory(t) { return new (t||X)(i0.ɵɵinvalidFactoryDep(0)); };
// setClassMetadata emits { type: undefined, decorators: [{ type: Optional }] }
Expected
The reference compiler filters null literal type nodes out of the union; if exactly one type remains, it becomes the token. Token resolution and @Optional() are independent — @Optional() only sets injection flag 8.
@Optional() svc: MyService | null → ɵɵdirectiveInject(MyService, 8)
svc: MyService | null → ɵɵdirectiveInject(MyService)
Reference: getConstructorParameters() in compiler-cli/src/ngtsc/reflection/src/typescript.ts — filters SyntaxKind.NullKeyword literal type nodes, narrows to the sole remaining type. undefined is intentionally not stripped.
Scope note: Genuinely ambiguous unions (A | B, two class types) should produce a compile diagnostic ("not supported as injection token"), not a silent runtime failure — tracked separately under diagnostics.
Impact
Hard runtime crash. Breaks the standard @Optional() x: T | null optional-DI pattern.
Researched and drafted with Claude Code, cross-checked against the Angular reference compiler source.
A constructor parameter typed as a class unioned with
null(MyService | null) fails token resolution. OXC emitsɵɵinvalidFactoryDep, which throws at instantiation. This is a token-resolution bug — independent of optionality.Repro (both fail)
Actual
Expected
The reference compiler filters
nullliteral type nodes out of the union; if exactly one type remains, it becomes the token. Token resolution and@Optional()are independent —@Optional()only sets injection flag8.@Optional() svc: MyService | null→ɵɵdirectiveInject(MyService, 8)svc: MyService | null→ɵɵdirectiveInject(MyService)Reference:
getConstructorParameters()incompiler-cli/src/ngtsc/reflection/src/typescript.ts— filtersSyntaxKind.NullKeywordliteral type nodes, narrows to the sole remaining type.undefinedis intentionally not stripped.Scope note: Genuinely ambiguous unions (
A | B, two class types) should produce a compile diagnostic ("not supported as injection token"), not a silent runtime failure — tracked separately under diagnostics.Impact
Hard runtime crash. Breaks the standard
@Optional() x: T | nulloptional-DI pattern.Researched and drafted with Claude Code, cross-checked against the Angular reference compiler source.