skip to content

After enabling strict templates, an Angular build fails because [user]="selected()" passes User | null to a required input; how do you diagnose and fix it?

level: seniorimportance: should knowfreq 46%

answer

  1. the binding is checked as an assignment
  2. null is part of the source type
  3. fix the data flow, not the checker
  4. assertions are promises you must keep

basics

~20 s

Angular's template checker treats the binding as an assignment under strictNullChecks, and User | null is not assignable to User. Render the child only when a user exists, or let the input accept null; reserve ! and $any() for values you can prove are set.

solid answer

~40 s

The error is TypeScript's `Type 'User | null' is not assignable to type 'User'`, reported at the binding in the parent template: strict mode checks `[user]="selected()"` as an assignment to `user = input.required<User>()`. It is usually a real bug - before a selection exists the child would receive `null`. Fixes, best first: render the child only when there is a value, for example `@if (selected(); as user) { <app-user-card [user]="user" /> }`; or change the child's contract to `input.required<User | null>()` and handle the empty state there. A non-null assertion, `selected()!`, is acceptable only when something outside the types guarantees the value. `$any(selected())` disables all checking of that expression. `strictNullInputTypes: false` relaxes null checks on every input in the app and is meant for third-party typings, not for your own components.

code

ts · 29 lines
ts
import { Component, input, signal } from '@angular/core';

interface User { name: string; }

@Component({
  selector: 'app-user-card',
  template: `<h2>{{ user().name }}</h2>`,
})
export class UserCard {
  user = input.required<User>();
}

@Component({
  selector: 'app-user-page',
  imports: [UserCard],
  template: `
    <!-- Error under strict templates: User | null is not assignable to User -->
    <!-- <app-user-card [user]="selected()" /> -->

    @if (selected(); as user) {
      <app-user-card [user]="user" />
    } @else {
      <p>Select a user</p>
    }
  `,
})
export class UserPage {
  selected = signal<User | null>(null);
}

go deeper

for a junior

Recognise that the error means the value might be null while the input requires a real object, and that the parent should avoid passing null.

for a middle

Explain the binding as an assignment checked under strictNullChecks and compare the fixes: conditional render, widening the input, assertion, $any.

for a senior

Separate real bugs from third-party typing gaps, choose the narrowest fix for each, and know why strictNullInputTypes is global and $any hides more than null.

for a principal

Decide how nullable state flows through a component hierarchy, where empty states live, so inputs can stay non-null and required by design.

## Reading the error With strict templates, the compiler turns `[user]="selected()"` into an assignment in the component's **type-check block**: roughly, `childInstance.user = ctx.selected();`. If `selected` is a `signal<User | null>(null)` and the child declares `user = input.required<User>()`, TypeScript reports: ```text error TS2322: Type 'User | null' is not assignable to type 'User'. Type 'null' is not assignable to type 'User'. ``` The location points at the binding in the parent's template (the inline template or the `templateUrl` file). The Language Service shows the same error in the editor. Two things to rule out first: - **It is not the "missing required input" error.** Omitting `[user]` entirely produces `NG8008`, *"Required input 'user' from component UserCard must be specified"*. Here the input *is* bound; its value's type is the problem. - **Is the source genuinely nullable?** Check `selected`'s declared type. If it starts as `null` until the user clicks, the error describes a real state the UI will reach. ## Why it is usually a real bug A required input promises the child always receives a `User`. Before anything is selected, the binding would pass `null`, and the child's template would try to read `user().name` on `null`. Strict mode caught at build time what would otherwise be a runtime `TypeError` on first render. ## The fixes, from best to last resort | Fix | What it changes | When to use it | | :-- | :-- | :-- | | Render only when set: `@if (selected(); as user)` | Parent passes a non-null `User` | The child has nothing meaningful to show without a user | | Widen the input: `input.required<User \| null>()` | Child accepts and handles `null` | The child owns an empty state | | Non-null assertion: `[user]="selected()!"` | Tells the checker "trust me" | Another guarantee exists that the types cannot express | | `$any(selected())` | Casts to `any`, disabling all checks on that expression | Almost never for this case: it hides more than nullability | | `strictNullInputTypes: false` | Drops null checks on **all** input bindings | Third-party inputs whose typings omit `null` | | `strictTemplates: false` | Turns strict mode off | Not a fix | How `@if` narrows the value is covered by the control-flow topic; the point here is that the parent stops passing `null`. ## The `async` pipe variant The same error appears with `[user]="user$ | async"`, because the `async` pipe's return type includes `null` for the moment before the first emission. The same fixes apply; if an assertion is justified, it needs parentheses: `[user]="(user$ | async)!"`. ## The third-party variant When the input belongs to a library compiled **without** `strictNullChecks`, its `.d.ts` omits `null` even though the library tolerates it. Your nullable value is then rejected through no fault of your own. This is the case `strictNullInputTypes: false` exists for. Because it applies to every input in the compilation, weigh it against asserting at the few affected call sites. ## Diagnosing systematically after turning strict mode on 1. Run the build and group errors by message; nullability errors usually dominate. 2. For each, decide whether the component really can receive `null` at runtime. 3. Real gaps: fix the parent (render conditionally) or the child (accept `null`). 4. Typing gaps in libraries: assert locally or relax `strictNullInputTypes`. 5. Keep `$any()` for genuinely untyped values, and leave a comment explaining why. ## Preventing it by design - Keep **required inputs non-nullable** and push the "nothing selected yet" state up to the component that owns the data. - Give that owner an explicit empty-state branch, so the decision about what to show without a user is made once. - When a child genuinely has an empty state, say so in its type (`User | null`) rather than asserting in every parent. - Treat each `!` or `$any()` in a template as a review item: it is a claim the compiler can no longer verify. ## What not to conclude - Do not make every input optional just to silence errors; that moves the null check into every child. - Do not assume the error is spurious because the app "works": it may work only because the child happens not to render until later.

  • When is a non-null assertion like [user]="selected()!" acceptable in an Angular template?
    When a guarantee exists that the type system cannot see, for example a route resolver that always provides the value before this view renders. The assertion only silences the checker; if the guarantee breaks, the child receives null at runtime. Prefer restructuring so the type itself is non-null, and comment any assertion you keep.
  • Why is $any() a poor fix for a nullable binding in an Angular template?
    `$any()` casts the whole expression to `any`, so the checker stops verifying not just null-ness but the entire type: a later refactor that binds a completely wrong object would also pass. A non-null assertion or a conditional render removes only the null case and keeps everything else checked.
  • What does NG8008 mean, and how does it differ from this nullability error?
    NG8008 reports that a required input was not bound at all on an element that matches the component. The nullability error means the input is bound but the expression's type includes null. Both are build-time checks; the fixes differ: add the binding for NG8008, fix the value's type for the other.

saying these in an interview costs you the question

  • Wrapping the value in $any() is the standard fix for a nullable input binding.
  • The error is a false positive because the app renders fine in the browser.
  • Setting strictNullInputTypes to false only affects the one failing component.
  • Making every input optional is the clean way to satisfy strict templates.
  • This is error NG8008, the missing-required-input error.