Why does Angular's signal-input migration skip some @Input fields, and how do you deal with the ones it leaves?
answer
- safe by default
- writes break a read-only signal
- inheritance, accessors, spies
- TODOs or best effort
basics
~20 sAngular's signal-input migration only converts inputs it can prove safe; fields written to, accessor inputs, inherited conflicts, spyOn targets, @HostBinding use or template narrowing are skipped. Use --insert-todos to see why, then fix by hand or opt into --best-effort-mode.
solid answer
~40 s`ng generate @angular/core:signal-input-migration` converts `@Input()` fields to `input()` and rewrites every reference to call the signal, in TypeScript, templates and host bindings. By default it **skips anything it cannot migrate safely**: inputs your code writes to (a signal input is read-only), getter/setter inputs, inputs overridden by or conflicting with a subclass, fields spied on with Jasmine's `spyOn`, inputs used with `@HostBinding`, inputs likely used for type narrowing in `@if`/`*ngIf`, and classes instantiated with `new`. `--insert-todos` adds a TODO explaining each skip; `--best-effort-mode` migrates more at the risk of a broken build. I would run it with TODOs per folder via `--path`, fix the skipped fields by refactoring (for example replacing input writes with a `linkedSignal` or a separate state field), then re-run. The queries migration works the same way for `@ViewChild` and friends.
code
ts · 15 linesimport { Component, Input } from '@angular/core';
@Component({
selector: 'app-pager',
template: `Page size: {{ pageSize }}`,
})
export class Pager {
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() pageSize = 10;
reset(): void {
this.pageSize = 10;
}
}go deeper
Know that the migration turns @Input fields into input() signals and updates every reference to call them.
Explain why written-to, accessor and @HostBinding inputs are skipped, and what --insert-todos and --best-effort-mode do.
Show how you would clear the leftovers: per-folder runs with TODOs, refactoring input writes into derived state, fixing spies and host bindings.
Decide whether a partial migration is acceptable long-term or whether the remaining decorator inputs justify refactoring work across teams.
The signal migrations convert decorator-based APIs to their function-based equivalents: `@Input()` to `input()`, `@ViewChild`/`@ViewChildren`/`@ContentChild`/`@ContentChildren` to `viewChild()`/`viewChildren()`/`contentChild()`/`contentChildren()`, and `@Output()` to `output()`. The input and query migrations share one engine, and its defining property is caution. ## What the input migration changes ```shell ng generate @angular/core:signal-input-migration ``` 1. Each `@Input()` class member becomes its `input()` equivalent, for example `@Input() name: string | undefined` becomes `readonly name = input<string>()`. 2. **Every reference** is updated to call the signal: `this.name` becomes `this.name()`, in TypeScript code, templates and host bindings. Where a value is read several times, it may introduce a local variable (`const name = this.name();`) to keep narrowing working. The same refactoring is offered in the VS Code extension as a code action on an `@Input` field. The `signals` schematic combines the input, output and query migrations in one run. ## Why fields are skipped A signal input is **read-only from inside the component** and must be read by calling it, so any code that treats the input as a plain mutable field cannot be converted blindly. The migration's skip reasons include: | Reason | Example | | :-- | :-- | | **Write assignment** | `this.pageSize = 20` in code, or a template or host binding that writes the field | | **Accessor input** | `@Input() set value(v) { ... }` | | **Inheritance conflicts** | A subclass overrides the field, re-declares it through an `inputs` array, or has a conflicting type | | **Jasmine `spyOn` on the field** | Spying overwrites the field, which breaks with signals | | **`@HostBinding` use** | `@HostBinding` does not call the signal, so the binding would break | | **Template narrowing** | The input is used in an `@if` or `*ngIf` condition in a way that narrows its type, which signals do not support yet | | **Manual instantiation** | `new MyComponent()`; signal inputs need an injection context | | **Required or optional input without an extractable type** | The migration cannot write a correct `input<T>()` type | Some reasons are never overridden, even by `--best-effort-mode`: fields outside the project's source files, fields filtered out of the current run, accessor inputs, and inputs whose type cannot be determined. ## The options | Option | Default | Effect | | :-- | :-- | :-- | | `--path` | whole workspace | Only migrate declarations under this path | | `--insert-todos` | off | Add a `// TODO: Skipped for migration because:` comment with the reason on each skipped input | | `--best-effort-mode` | off | Migrate as much as possible, even if the build may break | | `--analysis-dir` | whole workspace | Limit analysis for speed; references outside it are **silently skipped**, which can break the build | Note the difference between `--path` and `--analysis-dir`: `--path` limits what is converted while references everywhere are still updated; `--analysis-dir` limits what is even looked at. ## Dealing with the leftovers 1. **Run with `--insert-todos`** on one feature folder at a time, so each skip comes with its reason next to the code. 2. **Fix the pattern, not the symptom.** An input the component writes to usually hides local state: keep the input read-only and derive state from it, for example with `linkedSignal` or a separate field. 3. **Move `@HostBinding` to the `host` metadata** of the component, which does read signals, then re-run. 4. **Replace `spyOn` on inputs in tests** with setting the input through the test harness, such as `fixture.componentRef.setInput()`. 5. **Use `--best-effort-mode` only on a clean branch**, and read the resulting build errors as a to-do list. ## The same shape for queries and outputs The **queries migration** (`signal-queries-migration`) has the same options and adds its own skip reasons, such as `QueryList` members being accessed in ways it cannot translate. The **output migration** is simpler: it converts `@Output() x = new EventEmitter()` to `output()`, rewrites `.next()` to `.emit()`, removes `.complete()` calls, and skips outputs whose value is used with `.pipe()` outside tests or not initialised with `new EventEmitter`. What `input()`, `viewChild()` and `output()` are and how they behave is covered with component authoring; this topic is about what the tools can and cannot do for you.
- What is the difference between --path and --analysis-dir in Angular's signal-input migration?--path limits which inputs are converted, but the whole workspace is still analysed so every reference to a converted input is updated. --analysis-dir limits the analysis itself for speed, and references outside it are silently skipped, which can leave code calling a field that is now a signal and break the build.
- Why does a Jasmine spyOn on an input field block the signal-input migration?spyOn replaces the property with a spy function, which conflicts with the field becoming a signal that the component calls. The migration reports it as breaking with signals and skips the field; set the input through the fixture instead, for example with componentRef.setInput().
saying these in an interview costs you the question
- The signal-input migration converts every @Input, whatever the code does with it.
- Skipped inputs mean the migration crashed and must be re-run.
- --analysis-dir is a safe way to migrate one folder at a time.
- --best-effort-mode only adds comments and never breaks the build.
- Inputs used with @HostBinding migrate cleanly because host bindings read signals.