← All articles

@rdlabo/eslint-plugin-rules: The Angular ESLint Plugin I Wanted

Custom ESLint rules I built for Angular teams—Signal misuse, inject() migration, readonly for zoneless, Ionic lifecycle checks—with auto-fix where it helps.

Published
@rdlabo/eslint-plugin-rules: The Angular ESLint Plugin I Wanted cover image

The Angular ESLint plugin I wanted

When I develop Angular in a team, I often wish someone would auto-detect certain mistakes or enforce particular patterns. I built @rdlabo/eslint-plugin-rules to solve my own pain points—a collection of rules I needed on real projects, published for others.

For example, migrating to Signals leads to mistakes I see often. A pattern I personally miss:

name = signal<string | undefined>('Angular');

// ❌ Accidentally checking WritableSignal<string | undefined>
if (name) { ... }

// ✅ Correct version
if (name()) { ... }

name is a WritabeSignal<string>, so using it in a condition is always truthy—even when the value is empty or undefined. It compiles fine and only shows up in tests or manual checks. That was brutal, so I added a rule for it. The plugin is a rule set—some rules fit your team, some do not. If any look useful, I would love you to try them.

https://github.com/rdlabo-dev/eslint-plugin-ruleshttps://github.com/rdlabo-dev/eslint-plugin-rules

Available rules

This plugin provides the following custom ESLint rules:

@rdlabo/rules/deny-constructor-di

Prevents dependency injection in component and service constructors—encouraging migration from constructor DI to Angular's inject function.

// ❌ Traditional syntax
constructor(private http: HttpClient) {}

// ✅ Recommended syntax
private http = inject(HttpClient);

Auto-fix used to exist, but I removed it after ng generate @angular/core:inject shipped.

@rdlabo/rules/signal-use-as-signal

Validates correct Angular Signals usage. Detects direct mutation of signal values and incorrect signal access patterns.

// ❌ Invalid usage
this.user().name = data.name;
this.users().push(user);
if (this.user) { /* ... */ }

// ✅ Correct usage
this.user.update(user => ({ ...user, name: data.name }));
this.users.update(users => [...users, user]);
if (this.user()) { /* ... */ }

This rule existed before, but v20.0.0 overhauled it significantly and added auto-fix.

@rdlabo/rules/signal-use-as-signal-template

Enforces correct Signal usage in templates—you must call the signal function to read its value.

<!-- ❌ Invalid usage -->
{{ count }}
{{ user.name }}
@if (user) { /* ... */ }

<!-- ✅ Correct usage -->
{{ count() }}
{{ user().name }}
@if (user()) { /* ... */ }

Added in v20.0.0. Auto-fix is not available yet (following templateUrl means the lint target is not the file being fixed).

@rdlabo/rules/component-property-use-readonly

Requires the readonly modifier on class properties, especially Angular component properties.

// ❌ Missing readonly
private users: User[];
count: number;
name: string;

// ✅ Add readonly
private readonly users: User[];
readonly count: number;
readonly name: string;

This supports the "make all component properties readonly" step in the zoneless migration strategy I described here. Added in v20.0.0 with auto-fix.

https://zenn.dev/rdlabo/articles/c6623c6ccc16ddhttps://zenn.dev/rdlabo/articles/c6623c6ccc16dd

@rdlabo/rules/deny-soft-private-modifier

Disallows soft private modifiers and encourages explicit accessibility modifiers.

// ❌ Soft private modifier
private http = inject(HttpClient);

// ✅ Hard private modifier
#http = inject(HttpClient);

Auto-fix is available.

@rdlabo/rules/deny-element

Restricts specific HTML elements via configuration. I use it mainly in Ionic projects to ban direct modal/popover elements in templates and push usage through controllers.

<!-- ❌ Do not use directly in templates (for Ionic) -->
<ion-modal>
  <ion-content>...</ion-content>
</ion-modal>

<!-- ✅ Prefer using it from the controller -->
<!-- Use ModalController in TypeScript -->

@rdlabo/rules/deny-import-from-ionic-module

In Ionic projects, blocks direct imports from @ionic/angular and encourages finer-grained import paths.

Auto-fix is available.

@rdlabo/rules/implements-ionic-lifecycle

In Ionic projects, when you use lifecycle hooks (e.g. ionViewWillEnter), ensures the class implements the matching TypeScript interface (e.g. IonViewWillEnter).

// ✅ Correct implementation (for Ionic projects)
export class MyPage implements IonViewWillEnter {
  ionViewWillEnter() {
    // Lifecycle handling
  }
}

v20.0.0 strengthened this rule—it can auto-add missing lifecycle interfaces and remove unused ones from empty classes. Auto-fix is available.

Installation and configuration

Install the plugin:

npm install @rdlabo/eslint-plugin-rules --save-dev

One caveat: if angular-eslint is not installed, install it before this plugin.

# Install angular-eslint first if it is not installed
ng add angular-eslint

After installation, add the plugin and rules to your ESLint config (usually eslint.config.js) for TypeScript (*.ts) and HTML (*.html).

const rdlabo = require('@rdlabo/eslint-plugin-rules');

module.exports = tseslint.config(
  {
    files: ['*.ts'],
    plugins: {
      '@rdlabo/rules': rdlabo,
    },
    rules: {
      '@rdlabo/rules/deny-constructor-di': 'error',
      '@rdlabo/rules/deny-import-from-ionic-module': 'error',
      '@rdlabo/rules/implements-ionic-lifecycle': 'error',
      '@rdlabo/rules/deny-soft-private-modifier': 'error',
      '@rdlabo/rules/signal-use-as-signal': 'error',
      '@rdlabo/rules/signal-use-as-signal-template': 'error',
      '@rdlabo/rules/component-property-use-readonly': 'error',
    },
  },
  {
    files: ['*.html'],
    plugins: {
      '@rdlabo/rules': rdlabo,
    },
    rules: {
      // Configure only when using Ionic
      '@rdlabo/rules/deny-element': [
        'error',
        {
          elements: [
            'ion-modal',
            'ion-popover',
            'ion-toast',
            'ion-alert',
            'ion-loading',
            'ion-picker',
            'ion-action-sheet',
          ],
        },
      ],
    },
  }
);

Summary

@rdlabo/eslint-plugin-rules is a collection of custom ESLint rules to improve code quality and maintainability in Angular apps. v20.0.0 especially strengthened Signal support and added rules that help migrate toward zoneless change detection recommended in Angular v20.

These rules block specific anti-patterns and best-practice violations, often with auto-fix. They shine when you need to:

  • Enforce correct Angular Signals usage
  • Migrate from constructor DI to inject()
  • Prepare for zoneless change detection (readonly enforcement)
  • Use TypeScript accessibility modifiers correctly

The project is open source and accepts contributions. Extra rules for Ionic projects make it useful across many Angular codebases.
If you want better code quality in Angular development, consider trying it.

https://github.com/rdlabo-dev/eslint-plugin-ruleshttps://github.com/rdlabo-dev/eslint-plugin-rules

See you next time.