Angular Input Signals Explained: input(), input.required(), Transforms, Aliases [2026]

Link copied
Angular Input Signals Explained: input(), input.required(), Transforms, Aliases [2026]

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:

  1. Inputs are signals. You read them with name(), not name. The template auto-calls them.
  2. The generic type tells TypeScript what value comes in. input.required<string>() accepts only string from the parent; the compiler refuses anything else.
  3. Optional inputs need a default. input(40) infers number. 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 impossibleInputSignal 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).

YouI have a child that needs to react when an input changes. Do I need ngOnChanges?
Claude · used 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

View all Angular articles →
Angular

When Angular is launched ?

Link copied When Angular is launched ? When Angular Landed: A History of the Popular Web Framework # Angular, …

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *