feat(core): introduce @Service decorator#68195
Conversation
These changes introduce the new `@Service` decorator which is a more ergonomic alternative to `@Injectable`. The reason we're adding a new decorator is that `@Injectable` has been around since the beginning of Angular and it has a lot of baggage that adds unnecessary overhead for users that generally want to define a singleton service, available in their entire app. The key differences between `@Service` and `@Injectable` are:
1. `@Service` is `providedIn: 'root'` by default. You can opt into providing the service yourself by setting `autoProvided: false` on it.
2. `@Service` doesn't allow constructor-based injection, only the `inject` function.
3. `@Service` doesn't support the complex type signature of `@Injectable` (`useClass`, `useValue` etc.). Instead it supports a single `factory` function.
Example:
```ts
import {Service} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AuthService} from './auth';
@service()
export class PostService {
private readonly httpClient = inject(HttpClient);
private readonly authService = inject(AuthService);
getUserPosts() {
return this.httpClient.get('/api/posts/' + this.authService.userId);
}
}
```
|
you can refer to the functional dependency injection solution I have implemented 🤣 |
|
Deployed adev-preview for 4a65d03 to: https://ng-dev-previews-fw--pr-angular-angular-68195-adev-prev-gcwb19ii.web.app Note: As new commits are pushed to this pull request, this link is updated after the preview is rebuilt. |
|
Haha, pretty much what @JohannesHoppe foresaw last year 😬 https://angular.schule/blog/2025-09-service-decorator |
| /** | ||
| * A function to invoke to create a value for this service. | ||
| */ | ||
| factory?: () => unknown; |
There was a problem hiding this comment.
Q: Would we want to have some kind of typesafety on the factory ?
Like ensure that we returning something that matches the decorated class ? (not even sure if its doable).
There was a problem hiding this comment.
I'm not sure if the decorator type can be based on the class it's set on like this.
There was a problem hiding this comment.
you could do that. here's an example for non-experimental decorators:
declare function Service<T>(options?: {
factory?: () => NoInfer<T>
}): (target: abstract new (...args: any) => T) => void;
@Service({
factory: () => ({}), // TS2322: Type () => {} is not assignable to type () => NoInfer<Foo>. Property prop is missing in type {} but required in type Foo
})
export class Foo {
prop = 1;
}| } | ||
|
|
||
| const fixture = TestBed.createComponent(App); | ||
| fixture.detectChanges(); |
There was a problem hiding this comment.
Q: Is there a reason to use detectChanges instead of whenStable?
There was a problem hiding this comment.
I wanted to make sure that the query was evaluated.
|
I don’t think this would be a useful addition to Angular. There are already ESLint rules encouraging the use of inject() instead of constructors. Introducing a @service decorator would also make a large portion of existing articles, documentation, and videos—currently based on @Injectable({ providedIn: 'root' })—effectively outdated. It could also create confusion for AI tools. More importantly, I don’t see it bringing significant value. One of the main issues with services, in my opinion, is injecting services that are not properly provided. This leads to runtime errors that could potentially be avoided. I don’t think introducing a @service decorator would help address this problem. On the other hand, a more functional approach could start laying the groundwork to gradually solve this issue. |
|
With Angular 20, the service suffix has been removed from the style guide. While this leads to shorter filenames, it also makes the role of classes less obvious. It is no longer recommended to suffix components, services, and directives. That's generally a great idea. We get shorter filenames and more emphasis on deliberate naming. But there is one small downside: We no longer immediately recognize that a class is intended to be used as a service. until Angular 19 - the word "service" is somewhere // book-store.service.ts
@Injectable({
providedIn: 'root'
})
export class BookStoreService { }starting from Angular 20, WTF is this? // book-store.ts
@Injectable({
providedIn: 'root'
})
export class BookStore { }Of course, anyone who has used Angular for a while knows that the |
IMO, I also assume there would be some migrations in near future to update codebase in 1 go with new updates/latest feature migrations |
| }): ɵɵInjectableDeclaration<T> { | ||
| return { | ||
| token: opts.token, | ||
| providedIn: opts.autoProvided === false ? null : 'root', |
There was a problem hiding this comment.
The @Service() decorator is a great ergonomic improvement for the common case. However, there's a consistency concern worth discussing.
Since @Service() doesn't support providedIn: 'platform', codebases that need platform-scoped services end up mixing decorators:
@Service() // "normal" services
export class UserStore {}
@Injectable({ providedIn: 'platform' }) // wait, why is this different? I'm service too
export class UserStoreShared {}Both are conceptually services - only their scope differs. Having two decorators for the same concept based on configuration feels like it violates the principle of least surprise, especially for developers new to a codebase who'd have to understand why two seemingly identical things are declared differently.
Since @Service() already accepts options (autoProvided, factory), supporting providedIn: 'platform' wouldn't compromise the simplicity goal, and it would eliminate the mixed-decorator problem:
@Service({ providedIn: 'platform' })
export class UserStoreShared {}A useful parallel: Angular didn't split @Component into two decorators when standalone was introduced — it extended the existing one. The same logic could apply here.
Was this considered? Is the intent that @Injectable remains the decorator for "non-standard" DI configurations?
| }): ɵɵInjectableDeclaration<T> { | ||
| return { | ||
| token: opts.token, | ||
| providedIn: opts.autoProvided === false ? null : 'root', |
There was a problem hiding this comment.
I think it's worth asking the question. Why do we choose the 'root' injector and not the 'platform' which above 'root'
One argument against using platform, would be that support is port for several others features (#64090)
@ronnain I think you have solid points. I get you. But Angular is evolving all the time and I believe with new Service decorator (without having the all the baggage from Injectable) the team will be able to put more on to it and deliver new features and solutions. And I'm sure the community will provide articles, tutorials and all the rest of it. As a wise man once said, we move forward only and march to victory 😂 this wise man also mentions defeat but no need for that for us. |
AndrewKushnir
left a comment
There was a problem hiding this comment.
Reviewed-for: public-api
I believe that Angular needs to break free from the constraints of In my opinion, this matter not only increases the cognitive load for users, but also makes it difficult for beginners to get started. They don't know what to use, and they will only find Angular hard to pick up To describe this matter in Chinese, it's like “屎上雕花“ |
These changes introduce the new
@Servicedecorator which is a more ergonomic alternative to@Injectable. The reason we're adding a new decorator is that@Injectablehas been around since the beginning of Angular and it has a lot of baggage that adds unnecessary overhead for users that generally want to define a singleton service, available in their entire app. The key differences between@Serviceand@Injectableare:@ServiceisprovidedIn: 'root'by default. You can opt into providing the service yourself by settingautoProvided: falseon it.@Servicedoesn't allow constructor-based injection, only theinjectfunction.@Servicedoesn't support the complex type signature of@Injectable(useClass,useValueetc.). Instead it supports a singlefactoryfunction.Example: