Angular Input Signals Explained: input(), input.required(), Transforms, Aliases [2026]
An Angular component without inputs is an island. The input() function — added in v17.1 and the standard since v19 — is how a parent component hands data to a child. It replaces the older @Input() decorator with something simpler, type-safer, and integrated with the signal reactivity system.
This is lesson 2.4 of the Angular Tutorial, part of Module 2. By the end you will know every shape of input() — optional, required, transformed, aliased — and the gotchas that distinguish it from the legacy decorator.
The shape #
An input() call returns an InputSignal. You assign it to a class field at declaration time:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-avatar',
template: `<img [src]="src()" [alt]="name()" [style.width.px]="size()" />`,
})
export class AvatarComponent {
src = input.required<string>(); // required, no default
name = input(''); // optional, default ''
size = input(40); // optional, default 40
}
Three facts:
- Inputs are signals. You read them with
name(), notname. The template auto-calls them. - The generic type tells TypeScript what value comes in.
input.required<string>()accepts onlystringfrom the parent; the compiler refuses anything else. - Optional inputs need a default.
input(40)infersnumber.input<number | null>(null)if you want explicit null.
The parent passes values via property bindings (lesson 2.2):
<app-avatar [src]="user().avatarUrl" [name]="user().name" [size]="64" />
Required vs optional #
The two main flavors:
// Required — parent MUST pass a value; build fails if missing
userId = input.required<string>();
// Optional — default supplied; parent may pass or omit
pageSize = input(20);
With required inputs, the template compiler refuses <app-foo /> (missing userId) with a clear error. This is the v16+ replacement for the older pattern of declaring @Input() userId!: string and praying the parent sets it.
When to use required: when the component is useless without the value (an avatar without an image source, a user-card without a user). Use optional defaults for everything else.
Transforms #
An input can transform its incoming value with a one-line function. The most common case is parsing strings as booleans (HTML attributes are always strings):
import { input, booleanAttribute, numberAttribute } from '@angular/core';
@Component({...})
export class CardComponent {
// Without transform: parent must pass `[selected]="true"`
// With transform: parent can pass `selected` (no value, like an HTML attribute)
selected = input(false, { transform: booleanAttribute });
// Same idea for numbers — strings parse to numbers
size = input(40, { transform: numberAttribute });
// Custom transform — trim and lowercase
username = input('', { transform: (v: string) => v.trim().toLowerCase() });
}
Now <app-card selected> (with no ="true") is valid. The booleanAttribute helper handles '', 'true', 'false', and missing entirely. The numberAttribute helper parses numeric strings.
Transforms run once when the value comes in — they are not derived reactively. For derived state, compose a computed():
// Bad — transform runs every time value changes, but you can't easily react
fullName = input('', { transform: v => v.split(' ') });
// Good — keep the input as the source, derive with computed
fullName = input('');
nameParts = computed(() => this.fullName().split(' '));
Aliases — when the public name differs #
Sometimes the field name and the binding name should differ — usually for readability or to avoid collisions:
export class TooltipDirective {
// Field: 'config', Public name: 'appTooltip'
config = input.required<TooltipConfig>({ alias: 'appTooltip' });
}
The template uses the alias:
<button [appTooltip]="{ text: 'Save', side: 'top' }">Save</button>
Aliases are rare in component code. They are common in directive code (lesson 2.9) where the directive's selector is also one of its inputs.
Type narrowing on inputs #
A required input's signal is typed as the bare type — no null or undefined. An optional input with a default of null is T | null:
user = input.required<User>(); // signal<User>
selectedId = input<string | null>(null); // signal<string | null>
To narrow in the template:
@if (selectedId(); as id) {
<!-- id is string here, not string|null -->
<p>Selected: {{ id }}</p>
}
All of TypeScript's narrowing rules apply inside @if/@switch/@let.
Inputs in computed() and effect() #
Because inputs are signals, they participate in reactivity automatically:
import { Component, input, computed, effect } from '@angular/core';
@Component({...})
export class UserStatsComponent {
user = input.required<User>();
posts = input<Post[]>([]);
// Recomputes whenever user OR posts changes
ownPosts = computed(() => this.posts().filter(p => p.authorId === this.user().id));
constructor() {
effect(() => {
console.log(`User changed to: ${this.user().name}, posts: ${this.posts().length}`);
});
}
}
The computed() recalculates on input changes; the effect() fires on input changes. Both are reading the input signal, which Angular tracks as a dependency. Lesson 3.1 walks the signal graph in depth.
When parent's binding changes #
When a parent component re-renders with a new value for [user], Angular schedules an update to the child's user signal. The change appears synchronously at the next change-detection tick — components reading user() get the new value, computed() chains re-evaluate, effect() callbacks fire.
The key word is signal. There is no ngOnChanges lifecycle event you need to wire up; the signal IS the change notification.
Inputs and effect() — the one gotcha #
Reading an input inside an effect() is fine. Writing back to an input is impossible — InputSignal has no .set() or .update() method:
// ❌ This does not compile
this.user.set(newUser);
// ✅ Inputs are one-way: parent → child
If you need two-way data flow (parent provides AND can receive updates), use model() (lesson 3.3), not input(). The distinction is intentional — input() reflects the parent's source of truth.
Inputs vs constructor parameters #
For service injection, inject in the constructor or field (inject(...)). For parent-to-child data, use input(). Do not try to receive parent data through DI — it works only at injector setup time, which is too late for reactive updates.
Legacy decorator: @Input() #
The pre-v17 syntax still works:
// Legacy — supported, not recommended for new code
@Input() userId!: string;
@Input({ required: true }) user!: User;
@Input({ alias: 'appTooltip' }) config!: TooltipConfig;
Differences from input():
| Aspect | @Input() decorator |
input() function |
|---|---|---|
| Type | Plain field | InputSignal — must be called |
| Reactivity | Manual via ngOnChanges |
Built-in signal reactivity |
| Required at compile time | { required: true } works |
input.required<T>() — cleaner |
| Transform | transform: option |
transform: option |
| Reading in template | {{ user.name }} |
{{ user().name }} |
For new components, use input(). For migrating an old codebase, the patterns are mechanical 1:1 and the Angular CLI ships a migration: ng generate @angular/core:signal-input-migration.
Common gotchas #
| Symptom | Cause | Fix |
|---|---|---|
TypeError: this.user is not a function |
Forgot to call the signal — wrote this.user.name instead of this.user().name |
Add the parentheses; templates auto-call but TS code does not |
| Template doesn't update when parent changes input | The parent isn't actually re-rendering, OR you mutated an object instead of replacing it | Replace the object reference: this.users.set([...this.users(), newUser]) rather than this.users().push(newUser) |
| Required input never errors even when missing | The parent component's selector is wrong or the import is missing | Check the parent's template and imports array |
| Transform runs more often than expected | You expected memoization | Transform runs once per input change. Wrap derived state in computed() for memoization |
input() cannot be reassigned |
You tried this.user = ... |
InputSignal is read-only from the child. Use model() if you need two-way |
What's next #
Lesson 2.5 is the mirror image — output() for emitting events back up to the parent. Then lesson 2.6 handles <ng-content> for composing components from outside content. Lesson 2.7 catalogs lifecycle hooks. By the end of the Components & Templates module you will have the full toolkit for component composition.
Try it yourself #
A <color-pill> component with all four input flavors in one file:
import { Component, input, computed, booleanAttribute } from '@angular/core';
@Component({
selector: 'color-pill',
template: `
<span [style.background]="bg()" [style.opacity]="selected() ? 1 : 0.6">
{{ label() }} {{ count() > 0 ? '(' + count() + ')' : '' }}
</span>
`,
styles: `span { padding: 4px 12px; border-radius: 999px; color: white; }`,
})
export class ColorPillComponent {
label = input.required<string>();
count = input(0);
selected = input(false, { transform: booleanAttribute });
hue = input('teal', { alias: 'color' });
bg = computed(() => `var(--${this.hue()}, ${this.hue()})`);
}
Drop <color-pill label="Inbox" [count]="5" selected color="crimson" /> into a parent template. Every input form is on display, including a transform (booleanAttribute), an alias (color), and a computed derivation (bg).
get_best_practicesNo. input() is a signal, so anything reactive — computed() for derived state, effect() for side effects, the template itself — picks up changes automatically. The signal IS the change notification; you don’t need a separate lifecycle hook for it. Reach for ngOnChanges only when you’re maintaining legacy code that still uses @Input() decorators.Lesson 2.5 picks up output() — the other side of the parent-child conversation.
Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


