---
title: "Transform Input Properties in Angular: Decorators vs Signals"
date: "2023-11-10"
slug: "transform-inputs-properties-in-angular-easy"
author: "Dany Paredes"
canonical: "https://danywalls.com/transform-inputs-properties-in-angular-easy"
description: "Learn how to transform component inputs in Angular using @Input({ transform }) and modern Signal input({ transform }), with built-in booleanAttribute and numberAttribute helpers."
---


> ⚡ **Using Modern Angular (v17+)?** Jump directly to [Modern Signal Input Transforms with input()](#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:

```typescript
// ⚠️ 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:

```typescript
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:

```typescript
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()`:

```typescript
// 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:

```html
<!-- 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+) |
| :--- | :--- | :--- |
| **Reactivity** | Passive class property | Reactive 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) => boolean` | Explicit 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.
