Angular
·4 min read·↗

Transform Input Properties in Angular: Decorators vs Signals

Main cover illustration for article: Transform Input Properties in Angular: Decorators vs Signals
Summarize with AI:

⚡ Using Modern Angular (v17+)? Jump directly to Modern Signal Input Transforms with input() to see the idiomatic Signal API.

When designing reusable components, you often need to adapt or coerce incoming data:

  • Coercing string HTML attributes into booleans (e.g. <my-button disabled> passing "" instead of true).
  • Parsing query parameters or strings into numbers.
  • Mapping external domain models (like a Course[] list) into third-party UI models (like a SchedulerEvent[] for a calendar).

Historically, doing this required writing verbose getter and setter pairs.

In this practical guide, I will show you how to use Angular input transforms using both the @Input({ transform }) decorator and modern Signal input({ transform }).


The Problem: Verbose Getters and Setters

Before input transforms, coercing a boolean or formatting an object required this boilerplate:

// ⚠️ Legacy approach: Clunky getter/setter pair
@Component({ ... })
export class MyButtonComponent {
  private _disabled = false;
 
  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(value: boolean | string) {
    this._disabled = typeof value === 'boolean' ? value : value != null && `${value}` !== 'false';
  }
}

Now let's see how modern Angular solves this declaratively.


1. Built-in Coercion Helpers (booleanAttribute & numberAttribute)

Angular provides two built-in coercion functions:

import { Component, Input, booleanAttribute, numberAttribute } from '@angular/core';
 
@Component({
  selector: 'app-user-badge',
  standalone: true,
  template: `
    <span [class.active]="isActive">User ID: {{ userId }}</span>
  `,
})
export class UserBadgeComponent {
  // Coerces <app-user-badge is-active /> or "" into true
  @Input({ transform: booleanAttribute }) isActive = false;
 
  // Coerces "42" into 42 (number), fallback 0
  @Input({ transform: (val: unknown) => numberAttribute(val, 0) }) userId = 0;
}

2. Custom Domain Mapping Transforms

Input transforms are also great for adapting external data to internal contracts without polluting your component interface.

For example, imagine a Calendar component that accepts Course[] from your app but needs to pass SchedulerEvent[] to a third-party scheduler:

export interface Course {
  id: string;
  title: string;
  start: Date;
  description: string;
}
 
export interface SchedulerEvent {
  id: string;
  title: string;
  start: Date;
  end: Date;
  isAllDay: boolean;
}
 
function toScheduleEvents(courses: Course[]): SchedulerEvent[] {
  return (courses || []).map((course) => ({
    id: course.id,
    title: course.title,
    start: course.start,
    end: new Date(course.start.getTime() + 2 * 60 * 60 * 1000),
    isAllDay: false,
    description: course.description,
  }));
}

Modern Signal Input Transforms with input()

In modern Angular (v17.1+ / v19), we use Signal inputs with input() or input.required():

// calendar.component.ts (Modern Standalone & Signal Inputs)
import { Component, input, booleanAttribute } from '@angular/core';
import { Course, SchedulerEvent, toScheduleEvents } from './calendar.models';
 
@Component({
  selector: 'app-calendar',
  standalone: true,
  template: `
    <div class="calendar-wrapper">
      @if (showGrid()) {
        <p>Total Scheduled Events: {{ events().length }}</p>
 
        @for (event of events(); track event.id) {
          <div class="event-item">
            <strong>{{ event.title }}</strong> - {{ event.start | date:'shortTime' }}
          </div>
        }
      }
    </div>
  `,
})
export class CalendarComponent {
  // 1. Built-in boolean coercion with Signal inputs
  showGrid = input(true, { transform: booleanAttribute });
 
  // 2. Custom mapping transform: consumers pass Course[], component reads SchedulerEvent[]
  events = input.required<SchedulerEvent[], Course[]>({
    transform: toScheduleEvents,
  });
}

In the Consumer Template:

<!-- Consumers pass raw Course[] and boolean string attribute cleanly -->
<app-calendar [events]="myCoursesList" showGrid />

Decorator @Input vs Signal input() Comparison

Feature@Input({ transform }) (v16.1+)Signal input({ transform }) (v17.1+)
ReactivityPassive class propertyReactive Signal (events())
Required Inputs@Input({ required: true, transform: ... })input.required({ transform: ... })
Zoneless Ready🟡 Requires CD cycle✅ Surgical, fine-grained updates
Type Narrowing(val: string | boolean) => booleanExplicit write/read types via generics

Summary

Input transforms eliminate boilerplate getters/setters when coercing or mapping input data:

  1. Use booleanAttribute for boolean HTML flags (<app-card shadow />).
  2. Use numberAttribute for numerical attributes passed as strings.
  3. Use input({ transform }) with Signal inputs in modern Angular for reactive, type-safe data transformations.
Part of the Angular Series

These are my experiences learning and facing my daily challenges working with Angular.

View Entire Series

Frequently Asked Questions

What is the purpose of the transform option in Angular inputs?

The transform option formats, coerces, or maps raw incoming input values (like string attributes or external data models) into internal component types without writing cumbersome getter/setter pairs.

What built-in transform functions does Angular provide?

Angular provides booleanAttribute (which converts string flags like <app-btn disabled /> into true) and numberAttribute (which safely parses strings into numbers with fallback defaults).

How do you use input transformations with modern Angular Signal inputs?

Pass a transform function to the signal input options: input(false, { transform: booleanAttribute }) or input.required<User, RawUser>({ transform: toUser }).

Can a transform function change the TypeScript input type accepted by template consumers?

Yes. Angular input transforms support distinct write types (what the template consumer passes) and read types (what the component code reads internally).

Related Articles

Share this article

If you found this guide helpful, consider sharing it with your team or fellow developers.


Real Software. Real Lessons.

I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.

User avatar
User avatar
User avatar
User avatar
+13K

Join 13,800+ developers and readers.

No spam ever. Unsubscribe at any time.