- Notifications
You must be signed in to change notification settings - Fork26.4k
Closed
Description
Which @angular/* package(s) are relevant/related to the feature request?
core
Description
The interface collapse pattern (using a const with constructor interface) cannot use Angular decorators like@Injectable()
.
Key Use Case - Server-Side Rendering
interfaceCartService{events:Observable<CartEvent>;add(productId:string):Observable<AddProductResponse>;remove(productId:string):Observable<RemoveProductResponse>;}interfaceCartServiceConstructor{new():CartService;prototype:CartService;}@Injectable({providedIn:"root"})// TS errorconstCartService:CartServiceConstructor=class{ #http=inject(HttpClient); #events=newSubject<CartEvent>();events=this.#events.asObservable();add(productId:string):Observable<AddProductResponse>{// Implementation}remove(productId:string):Observable<RemoveProductResponse>{// Implementation}}// Clean noop implementation for SSRexportconstnoopCartService:CartService={events:NEVER,add(){returnNEVER;},remove(){returnNEVER;}};// In server moduleproviders:[{provide:CartService,useValue:noopCartService}]
Current Problem
With traditional class declarations, TypeScript requires noop implementations to include private members:
@Injectable({providedIn:'root'})classCartService{#http=inject(HttpClient);#events=newSubject<CartEvent>();events=this.#events.asObservable();add(productId:string):Observable<AddProductResponse>{returnthis.#http.post// Implementation().pipe(tap(()=>this.#events.next(newProductAddedEvent())));}remove(productId:string):Observable<RemoveProductResponse>{returnthis.#http.post// Implementation().pipe(tap(()=>this.#events.next(newProductRemovedEvent())));}};// ❌ TypeScript error - must implement private #http, #events@Injectable({providedIn:'root'})classNoopCartServiceimplementsCartService{events=NEVER;add(){returnNEVER;}remove(){returnNEVER;}}
And abstract classes lead to situations where devs can easily make a mistake (especially junior engineers)
abstractclassAbstractCartService{#http=inject(HttpClient);#events=newSubject<CartEvent>();events=this.#events.asObservable();abstractadd(productId:string):Observable<AddProductResponse>abstractremove(productId:string):Observable<RemoveProductResponse>;};@Injectable({providedIn:'root'})classCartServiceextendsAbstractCartService{#http=inject(HttpClient);#events=newSubject<CartEvent>();events=this.#events.asObservable();add(productId:string):Observable<AddProductResponse>{// Implementation}remove(productId:string):Observable<RemoveProductResponse>{// Implementation}// This should have been added to the abstract class.removeAll():Observable<RemoveAllProductsResponse>{// Implementation}};exportconstnoopCartService:CartService={events:NEVER,add(){returnNEVER}remove(){returnNEVER}// BUG - the noop variation did not require the removeAll method when it actually needs it.};
Proposed solution
Some public variation of thedefineInjectable
constCartService:CartServiceConstructor=class{// Obviously wouldn't use the internal symbolsstaticɵprov=/**@pureOrBreakMyCode *//*@__PURE__ */ɵɵdefineInjectable({token:CartService,providedIn:'root',factory:()=>newCartService(),});};
Alternatives considered
- Using
InjectionToken
's. I have found in our org (which maintains 7ish angular applications) junior Angular engineers expect a service to be a class so this one tends to confuse them. Not a big deal to workaround though
constCART_SERVICE=newInjectionToken('CART_SERVICE',{factory:()=>newCartService(),});
- Defining a
CartManager
seperately from theCartService
(my current favorite workaround). This still requires some social enforcement but it's better than the abstract class.
interfaceCartManager{events:Observable<CartEvent>;add(productId:string):Observable<AddProductResponse>;remove(productId:string):Observable<RemoveProductResponse>;}interfaceCartManagerConstructor{new():CartManager;prototype:CartManager;}constCartManager:CartManagerConstructor=class{#http=inject(HttpClient);#events=newSubject<CartEvent>();events=this.#events.asObservable();add(productId:string):Observable<AddProductResponse>{// Implementation}remove(productId:string):Observable<RemoveProductResponse>{// Implementation}};@Injectable({providedIn:'root'})// This service purely exists to apply the@Injectable decorator.exportclassCartServiceextendsCartManager{// Do NOT add any logic here. Add it in the CartManager instead.}
Metadata
Metadata
Assignees
Labels
No labels