Skip to content

Add support for Angular @Service decorator (only Angular 22+) #3020

Description

@splincode

Description

Angular is introducing a new @Service() decorator for dependency injection services.

It would be useful for angular-eslint to support this decorator as a first-class Angular decorator and optionally provide a rule that prefers @Service() over @Injectable({providedIn: 'root'}) when the migration is safe.

Related Angular PR: angular/angular#68506

Motivation

Today, root-provided Angular services are commonly declared with @Injectable({providedIn: 'root'}):

import {Injectable} from '@angular/core';

@Injectable({providedIn: 'root'})
export class UserService {}

With the new @Service() decorator, the same root-provided service can be expressed more directly:

import {Service} from '@angular/core';

@Service()
export class UserService {}

Angular itself has started using @Service() in places where @Injectable({providedIn: 'root'}) was previously used.

For factory-based services, the Angular PR also shows this migration pattern:

@Injectable({
  providedIn: 'root',
  useFactory: () => new NgLocaleLocalization(inject(LOCALE_ID)),
})

to:

@Service({
  factory: () => new NgLocaleLocalization(inject(LOCALE_ID)),
})

Proposed rule

Add a new rule that reports @Injectable({providedIn: 'root'}) when it can be safely replaced with @Service().

Possible rule names:

  • prefer-service-decorator
  • prefer-angular-service
  • prefer-service-over-injectable

I personally prefer prefer-service-decorator, because it is short and directly describes the desired Angular decorator.

Invalid examples

import {Injectable} from '@angular/core';

@Injectable({providedIn: 'root'})
export class UserService {}
import {Injectable} from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UserService {}
import {Injectable, inject} from '@angular/core';

@Injectable({
  providedIn: 'root',
  useFactory: () => inject(UserServiceImpl),
})
export abstract class UserService {}

Valid examples

import {Service} from '@angular/core';

@Service()
export class UserService {}
import {Service, inject} from '@angular/core';

@Service({
  factory: () => inject(UserServiceImpl),
})
export abstract class UserService {}
import {Injectable} from '@angular/core';

@Injectable()
export class LocallyProvidedService {}
import {Injectable} from '@angular/core';

@Injectable({providedIn: SomeModule})
export class ModuleScopedService {}
import {Injectable} from '@angular/core';

@Injectable({providedIn: 'platform'})
export class PlatformService {}
import {Injectable} from '@angular/core';

@Injectable({providedIn: 'any'})
export class AnyScopedService {}

Auto-fix

The rule could provide an auto-fix for simple and safe cases.

Basic case

Before:

import {Injectable} from '@angular/core';

@Injectable({providedIn: 'root'})
export class UserService {}

After:

import {Service} from '@angular/core';

@Service()
export class UserService {}

Factory case

Before:

import {Injectable, inject} from '@angular/core';

@Injectable({
  providedIn: 'root',
  useFactory: () => inject(UserServiceImpl),
})
export abstract class UserService {}

After:

import {Service, inject} from '@angular/core';

@Service({
  factory: () => inject(UserServiceImpl),
})
export abstract class UserService {}

Conservative auto-fix conditions

The auto-fix should probably be conservative at first.

It can safely fix:

  • @Injectable({providedIn: 'root'})
  • @Injectable({ providedIn: 'root' })
  • @Injectable({providedIn: 'root', useFactory: ...})

It should not auto-fix yet:

  • @Injectable()
  • @Injectable({providedIn: null})
  • @Injectable({providedIn: 'platform'})
  • @Injectable({providedIn: 'any'})
  • @Injectable({providedIn: SomeModule})
  • @Injectable({useClass: ...})
  • @Injectable({useExisting: ...})
  • @Injectable({useValue: ...})
  • cases where the migration semantics are not confirmed by Angular yet

Suggested configuration

{
  "@angular-eslint/prefer-service-decorator": "error"
}

or with options:

{
  "@angular-eslint/prefer-service-decorator": [
    "error",
    {
      "fixFactories": true
    }
  ]
}

Why this belongs in angular-eslint

angular-eslint already understands Angular decorators and provides rules around Angular-specific class metadata.

Since @Service() is a new Angular DI decorator, angular-eslint should recognize it as a first-class Angular decorator and help teams migrate when the migration is safe.

This would also help avoid custom project-level lint rules once @Service() becomes the recommended style for root-provided services.

Versioning / stability note

This request depends on the Angular @Service() API becoming publicly available.

Until the Angular PR lands, this rule could be experimental or version-gated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    package: eslint-pluginAngular-specific TypeScript rulestriageThis issue needs to be looked at and categorized by a maintainer

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions