Angular’s Extended Diagnostics: The Key to Boosting Your Code Quality

Mariodante
6 min readSep 30, 2024

--

Angular 18’s Extended Diagnostics: The Key to Boosting Your Code Quality

Hello there!

In a previous Angular version (specifically, Angular v13), a significant new feature has been added to the compiler: extended diagnostics. Let’s explore this enhancement in more detail.

Why Extended Diagnostics Matter in Angular

As modern web applications grow increasingly complex, maintaining clean, efficient, and error-free code becomes more challenging. Angular introduces a powerful tool to help developers catch subtle issues early: extended diagnostics.

Unlike traditional TypeScript errors, which often focus on critical compilation issues, extended diagnostics in Angular delve deeper, identifying potential problems before they manifest. Whether it’s preventing unintended behaviors or guiding developers toward best practices, this feature ensures your code remains robust and maintainable in the long run.

Severities — Extended diagnostics configurations

Before we delve into configuration, let’s understand the different severity levels.
Extended diagnostics are warnings by default and do not block compilation. Each diagnostic can be configured with one of the following severities:

  • warning: The compiler emits the diagnostic but continues compilation. A warning severity won’t cause compilation failure. In this case, a yellow message is shown in your console.
  • error: The compiler emits the diagnostic and fails the compilation. This results in a non-zero exit code. Using this severity, a red message is shown in your console (in addition to failing the compilation).
  • suppress: The compiler completely hides the diagnostic.

Setting up: Enabling extended diagnostics

To enable extended diagnostics in your Angular project, you need to configure it within your tsconfig.json file under the angularCompilerOptions. Below is an example of how you can add this setting:

{
"angularCompilerOptions": {
"extendedDiagnostics": {
"checks": {
//use your prefer setting
"invalidBananaInBox": "error",
"nullishCoalescingNotNullable": "warning",
"missingControlFlowDirective": "error",
"textAttributeNotBinding": "warning",
"missingNgForOfLet": "error",
"suffixNotSupported": "warning",
"optionalChainNotNullable": "warning",
"skipHydrationNotStatic": "error",
"interpolatedSignalNotInvoked": "warning",
"uninvokedFunctionInEventBinding": "suppress"
}
}
}
}

This configuration enables extended diagnostics and defines different levels (error, warning, suppress) for specific checks. You can customize it according to your project’s needs.

Real-World Examples of Extended Diagnostics

Let’s dive into some of the most useful diagnostics checks Angular offers:

1. NG8101: invalidBananaInBox

This error occurs when you incorrectly bind to an input event using banana-in-a-box syntax [(...)]. This syntax is used for two-way data binding, and improper use can lead to unexpected behavior.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<input ([ngModel])="userName">`,
})
class MyComponent {}

The correct usage should be:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<input [(ngModel)]="userName">`,
})
class MyComponent {}

You have to notice that the incorrect example we’re using ([var]) instead of [(var)]. In case to use ([var]), you are doing an event binding to a model variable called [var].

2.NG8102: nullishCoalescingNotNullable

This diagnostic detects a useless nullish coalescing operator (??) characters in Angular templates. Specifically, it looks for operations where the input is not "nullable", meaning its type does not include null or undefined. For such values, the right side of the ?? will never be used.

Example:

import {Component} from '@angular/core';
@Component({
template: `<div>{{ ng8102 ?? "default text" }}</div>`,
})
class MyComponent {
ng8102: string | null = 'ng8102';
}

The correct usage should be:

import {Component} from '@angular/core';
@Component({
template: `<div>{{ ng8102 }}</div>`,
})
class MyComponent {
ng8102: string = 'ng8102';
}

In this case, you have to notice that using the nullish coalescing operator with a non-nullable input has no effect, revealing a mismatch between the value’s allowed type and its template presentation.

3. NG8103: missingControlFlowDirective

This error is thrown when Angular expects a control flow directive like *ngIf or *ngFor, but none is found. This can happen when you forget to include the directive on elements that require them.

Example:

import {Component} from '@angular/core';
@Component({
standalone: true,
imports: [],
template: `<div *ngIf="visibleNg8103">ng8103</div>`,
})
class MyComponent {
visibleNg8103 = true;
ng8103: string = 'ng8103';
}

The correct usage should be:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `<div *ngIf="visibleNg8103">ng8103</div>`,
})
class MyComponent {
visibleNg8103 = true;
ng8103: string = 'ng8103';
}

In this case, you have to notice that you are not importing the correct module; therefore, Angular tries to bind a property with an unknown directive.

4. NG8104: textAttributeNotBinding

This diagnostic warns you when you use a text attribute instead of property binding. In Angular, data binding should generally be done using [property] syntax rather than relying on static HTML attributes.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<div attr.id="ng8104_id">incorrect</div>`,
})
class MyComponent {}

The recommended approach is:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<div [attr.id]="ng8104_id">Correct</div>`,
})
class MyComponent {}

In this case, you have to notice that you are not binding correctly your element.
When binding to attr., class., or style., ensure you use the Angular template binding syntax ([]).
<div [attr.id]=”my-id”></div>
<div [style.color]=”red”></div>
<div [class.large]=”true”></div>

5. NG8105: missingNgForOfLet

This error points out missing let declarations when using the *ngFor directive.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<li *ngFor="item of items">{{ item }}</li>`,
})
class MyComponent {
items = ['item1', 'item2', 'item3'];
}

The correct way:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<li *ngFor="let item of items">{{ item }}</li>`,
})
class MyComponent {
items = ['item1', 'item2', 'item3'];
}

6. NG8106: suffixNotSupported

This warning highlights when you use an unsupported suffix with Angular binding expressions, particularly in directive-based structures.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<img src="incorrect" [attr.width.px]="ng8106" /> <!-- Warning: NG8106 -->`,
})
class MyComponent {
ng8106 = 5;
}

The correct usage should be:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [],
template: `<img src="correct" [attr.width]="ng8106" />`,
})
class MyComponent {
ng8106 = '5px';
}

In this case, the correctly use of directive is [attr.width].

7. NG8107: optionalChainNotNullable

This diagnostic warns when you use optional chaining (?.) on an expression that can never be null or undefined.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `<div>Incorrect: {{ ng8107?.name }}</div>`,
})
class MyComponent {
ng8107: { name: string } = { name: 'ng8107' };
}

The correct way:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `<div>Correct: {{ ng8107.name }}</div>`,
})
class MyComponent {
ng8107: { name: string } = { name: 'ng8107' };
}

In this case, using the optional chaining operator with a non-nullable input has no effect, indicating a mismatch between the allowed type and its template presentation. Developers may mistakenly assume the output could be null or undefined, leading to confusion about the program’s expected output.

9. NG8109: interpolatedSignalNotInvoked

Angular flags this warning when interpolated signals are not invoked correctly within templates. This diagnostic helps ensure signals function as expected in reactive contexts.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `<div>{{ ng8109 }}Incorrect</div>`,
})
class MyComponent {}

It should be:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `<div>{{ ng8109() }}Correct</div>`,
})
class MyComponent {}

In this case, the correct usage of any signal in the template is var().

10. NG8111: uninvokedFunctionInEventBinding

This error appears when a function is referenced but not invoked in an event binding.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `<button (click)="handleClick">Click me</button>`,
})
class MyComponent {}

The correct way:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `<button (click)="handleClick()">Click me</button>`,
})
class MyComponent {}

11. NG8112: unusedLetDeclaration

This error appears when a @let declaration is declared but never used.

Example:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `@let incorrect =''; `,
})
class MyComponent {}

The correct way:

import {Component} from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `@let correct ='';
<div>{{correct}}</div>`,
})
class MyComponent {}

This error appears when a @let declaration is declared but never used

At the time this document was created, Angular is in the process of adding more configurations for its upcoming v19 release.

Conclusion

Extended diagnostics not only help you prevent errors, but they also guide you toward best practices, improving the maintainability and quality of your code. Activate them and elevate the quality of your Angular applications starting today!

Happy coding, thanks for your attention!

Resources

Disclaimer: code shared in this article is just a basic implementation to
demonstrate each configuration settings.

https://github.com/mariodantemariani/angular-18-extended-diagnostics

References

https://angular.dev/extended-diagnostics

--

--

No responses yet