Skip to content

Activated flag strictNullInputTypes on Angular 21 - #7151

Open
cdavalos7 wants to merge 2 commits into
tensorflow:masterfrom
cdavalos7:feature/strict-null-input-types-on-21
Open

Activated flag strictNullInputTypes on Angular 21#7151
cdavalos7 wants to merge 2 commits into
tensorflow:masterfrom
cdavalos7:feature/strict-null-input-types-on-21

Conversation

@cdavalos7

@cdavalos7 cdavalos7 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Motivation for features / changes

Enable strictNullInputTypes to strengthen Angular template type safety, catch nullable input bindings at build time, and prevent UI errors caused by loading or unavailable data. The change also modernizes synchronous NgRx selector bindings with Angular signals and reduces redundant reactive subscriptions.

Technical description of changes

  • Enabled strictNullInputTypes: true in tsconfig.json.
  • Migrated synchronous NgRx selector bindings from Observable + AsyncPipe to selectSignal or toSignal({requireSync: true}).
  • Kept AsyncPipe for genuinely asynchronous streams and widened their receiving inputs when null is a valid value.
  • Updated input types, template guards, and fallback values to handle null and undefined explicitly.
  • Updated tests and NgRx mocks to provide synchronous selector state where required.
  • Simplified three containers by replacing duplicate RxJS-to-Signal conversions with computed derived state.

@cdavalos7 cdavalos7 changed the title Activated flag strictNullInputTypes, causing breaking async pipes is … Activated flag strictNullInputTypes on Angular 21 Aug 26, 2026
@cdavalos7
cdavalos7 marked this pull request as ready for review September 1, 2026 21:05
@cdavalos7
cdavalos7 requested a review from arcra September 1, 2026 22:07

@arcra arcra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at least we should aim to remove the ! characters, possibly define default values.

});
})
)
this.numAlerts = this.store.selectSignal(getNumAlerts);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to change it in this PR if you prefer the alternative of adding the type to the variables, but in the future, the recommended pattern is to use inject() and direct assignment of the signals outside of the constructor (and the constructor can likely be removed).

})
export class AlertsContainer {
readonly numAlerts$;
readonly numAlerts;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment below, but if we're not assigning something directly to these, we should specify the type for each of them (although at that point, then I might prefer the direct assignment).

AFAIU, with a direct assignment at declaration, the static analyzer can infer the type, and thus, also signal when the variable is not used as it should be used. I don't know if it's the same for these without a type. I would imagine that the static analyzer would consider them to have type any, which would not be helpful to catch errors.

Although... since they're declared as "readonly" ... it's possible that the analyzer knows how to infer the type from the assignment in the constructor... I don't really know (google says that that's not the case).

I recognize this is also not introduced here, but that's how it was before, so maybe it's ok to leave it as is, but in the future, let's prefer assignment at declaration.

)
this.numAlerts = this.store.selectSignal(getNumAlerts);
this.alertsBreakdown = this.store.selectSignal(
createSelector(getAlertsBreakdown, (alertsBreakdown) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to change it in this PR, but wondering what you think about these being computed signals instead of a selector defined inline?

I guess we'd need to first have a signal for the existing selector and then a separate computed signal for the thing we actually care about... which is a bit annoying.

)
)
this.tensorDebugMode = this.store.selectSignal(
createSelector(getFocusedExecutionData, (execution: Execution | null) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one should have more clearly been a computed signal, since we already have a signal for the selector used here.

(But again, no need to change it here. I understand you might have just got for a simple code update, especially considering the amount of files modified.)

export class GraphComponent {
@Input()
opInfo!: GraphOpInfo;
opInfo!: GraphOpInfo | null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this variable have a !, and then also a | null type?

Is the ! character saying that we're expecting it to never be null or undefined? I think this needs to be updated?

readonly focusedSourceLineSpec;

readonly useDarkMode$: Observable<boolean>;
readonly useDarkMode;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one had a type, and it was removed. I think this is a regression. This should be Signal ?

readonly mode;
readonly xAxisType;
readonly showFullWidth;
isPinned!: Signal<boolean>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again for these ! symbols. Let's set a default value? Here and above.

this.steps$ = this.store.select(getMetricsImageCardSteps, this.cardId);
const steps$ = this.store.select(getMetricsImageCardSteps, this.cardId);
this.steps = toSignal(steps$, {
injector: this.injector,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, why was it necessary to specify this?

);
this.showPaginationControls$ = this.numPages$.pipe(
map((numPages) => numPages > 1)
this.isGroupExpanded = toSignal(this.isGroupExpanded$, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple other examples where these should have been computed signals.

If you'd like to make these changes, we can also have separate PRs with smaller scope. e.g. updating only one subdirectory at a time, or something.

this.notificationNotes = toSignal(notificationNotes$, {
requireSync: true,
});
this.hasUnreadMessages = computed(() =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh... this one did become a computed signal...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants