Pipes are Angular’s template transformation functions. They take a value, optionally transform it with parameters, and return a new display value — without modifying the original data. Pipes keep transformation logic out of components and templates, make templates more readable, and enable reuse across the application. The built-in pipes cover the most common formatting needs, while custom pipes let you encapsulate any domain-specific transformation. The async pipe is particularly powerful — it automatically subscribes to Observables and Promises, unwraps their values, and unsubscribes when the component is destroyed, eliminating a common source of memory leaks.
Built-in Pipe Reference
| Pipe | Transforms | Example | Output |
|---|---|---|---|
date |
Date to string | now | date:'mediumDate' |
Jan 15, 2025 |
date |
Date to string | now | date:'HH:mm' |
14:30 |
currency |
Number to currency | 99.9 | currency:'USD' |
$99.90 |
number |
Number formatting | 3.14159 | number:'1.2-2' |
3.14 |
percent |
Number to percent | 0.85 | percent |
85% |
uppercase |
String to uppercase | 'hello' | uppercase |
HELLO |
lowercase |
String to lowercase | 'WORLD' | lowercase |
world |
titlecase |
First letter of each word capitalised | 'in progress' | titlecase |
In Progress |
slice |
Array/string slice | items | slice:0:5 |
First 5 items |
json |
Object to JSON string | obj | json |
Formatted JSON (debug only) |
keyvalue |
Object to key-value pairs | obj | keyvalue |
Array of { key, value } |
async |
Observable/Promise to value | tasks$ | async |
Latest emitted value or null |
i18nPlural |
Pluralisation | count | i18nPlural:mapping |
Contextual string |
Pure vs Impure Pipes
| Type | Recalculated When | Performance | Use For |
|---|---|---|---|
| Pure (default) | Input value or reference changes | Excellent — cached between checks | Most pipes — date, currency, filter by value |
Impure (pure: false) |
Every change detection cycle | Expensive — runs many times per second | Pipes that depend on mutable state outside inputs (e.g. current time) |
tasks$ = this.taskService.getAll() in the class and *ngFor="let task of tasks$ | async" in the template is cleaner, safer, and more reactive than manually subscribing in ngOnInit and unsubscribing in ngOnDestroy.ng-container and the as syntax: *ngIf="tasks$ | async as tasks". This subscribes once, stores the value in the template variable tasks, and exposes both the subscription and the value in the same block. Without as, each | async in the template creates a separate subscription.transform() method runs on every change detection cycle — which can be dozens of times per second in an active application. For filtering or sorting lists, compute the filtered result in the component class using signals (computed()) or ngOnChanges() and bind the computed value in the template — never use an impure pipe for this.Complete Pipe Examples
<!-- ── Built-in pipes ──────────────────────────────────────────────────── -->
<!-- Date pipe with format string -->
<p>Created: {{ task.createdAt | date:'MMM d, y HH:mm' }}</p>
<p>Due: {{ task.dueDate | date:'mediumDate' }}</p>
<p>Updated: {{ task.updatedAt | date:'relative' }}</p>
<!-- Chaining pipes -->
<p>{{ task.title | uppercase | slice:0:50 }}</p>
<p>{{ task.status | titlecase }}</p>
<!-- Number formatting -->
<p>Price: {{ product.price | currency:'EUR':'symbol':'1.2-2' }}</p>
<p>Progress: {{ completionRate | percent:'1.0-1' }}</p>
<p>Score: {{ score | number:'1.1-2' }}</p>
<!-- Slice pipe -->
<span *ngFor="let tag of task.tags | slice:0:3">{{ tag }}</span>
<span *ngIf="task.tags.length > 3">+{{ task.tags.length - 3 }} more</span>
<!-- keyvalue pipe — iterate object properties -->
<div *ngFor="let item of taskStats | keyvalue">
{{ item.key }}: {{ item.value }}
</div>
<!-- json pipe — debug display -->
<pre>{{ task | json }}</pre>
<!-- ── Async pipe ──────────────────────────────────────────────────────── -->
<!-- Simple async — subscribes automatically, unsubscribes on destroy -->
<li *ngFor="let task of tasks$ | async; trackBy: trackById">
{{ task.title }}
</li>
<!-- Async with as — subscribe once, use value multiple times -->
<ng-container *ngIf="tasks$ | async as tasks">
<p>{{ tasks.length }} tasks</p>
<li *ngFor="let task of tasks; trackBy: trackById">
{{ task.title }}
</li>
</ng-container>
<!-- Handle loading and error states with async -->
<ng-container *ngIf="{ tasks: tasks$ | async, loading: loading$ | async } as vm">
<app-spinner *ngIf="vm.loading"></app-spinner>
<ul *ngIf="!vm.loading && vm.tasks">
<li *ngFor="let task of vm.tasks; trackBy: trackById">{{ task.title }}</li>
</ul>
</ng-container>
// ── Custom pure pipe ──────────────────────────────────────────────────────
// ng generate pipe shared/pipes/relative-date
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'relativeDate',
standalone: true,
pure: true, // default — only recalculates when input changes
})
export class RelativeDatePipe implements PipeTransform {
transform(value: string | Date | null | undefined): string {
if (!value) return '';
const date = value instanceof Date ? value : new Date(value);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours= Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
return `${Math.floor(diffDays / 365)}y ago`;
}
}
// ── Custom pure pipe with parameters ──────────────────────────────────────
@Pipe({ name: 'truncate', standalone: true, pure: true })
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength = 100, ellipsis = '...'): string {
if (!value || value.length <= maxLength) return value;
return value.substring(0, maxLength).trimEnd() + ellipsis;
}
}
// Usage: {{ task.description | truncate:200:'…' }}
// ── Custom pure pipe — priority label ────────────────────────────────────
@Pipe({ name: 'priorityLabel', standalone: true })
export class PriorityLabelPipe implements PipeTransform {
private labels: Record<string, string> = {
low: '🟢 Low',
medium: '🟡 Medium',
high: '🔴 High',
};
transform(priority: string): string {
return this.labels[priority] ?? priority;
}
}
// ── Using pipes in component class (not just templates) ───────────────────
import { DatePipe } from '@angular/common';
@Component({ ... })
export class TaskExportComponent {
private datePipe = inject(DatePipe);
exportToCSV(tasks: Task[]): string {
const rows = tasks.map(t => [
t.title,
t.status,
this.datePipe.transform(t.createdAt, 'yyyy-MM-dd') ?? '',
]);
return rows.map(r => r.join(',')).join('\n');
}
}
// Must add DatePipe to providers or import DatePipe in component imports
How It Works
Step 1 — Pipes Transform Values Without Side Effects
A pipe’s transform() method receives a value (and optional parameters), computes a new value, and returns it. It must not modify the input or any external state — pure functions only. The return value is what Angular renders in the template. The original data in the component class is unchanged. Multiple templates can pipe the same data through different transformations without interfering with each other.
Step 2 — Pure Pipes Are Memoized
Angular caches the result of a pure pipe and only calls transform() again when the input value changes (by reference for objects/arrays, by value for primitives). If a component’s change detection runs 60 times per second and the date field has not changed, the date pipe returns the cached string without calling transform() 60 times. This makes pure pipes very efficient even in templates that are checked frequently.
Step 3 — The Async Pipe Manages Observable Subscriptions
When Angular renders {{ observable$ | async }}, the async pipe calls observable$.subscribe() and stores the subscription. Each emitted value updates the template. When the component is destroyed (navigating away, *ngIf becomes false), Angular calls the pipe’s ngOnDestroy() which calls subscription.unsubscribe(). This lifecycle management is automatic — you never write a Subject + takeUntil pattern when using the async pipe correctly.
Step 4 — Pipe Chaining Applies Transformations in Sequence
{{ value | pipe1 | pipe2:arg | pipe3 }} applies pipe1 first, passes its output to pipe2 with the argument, then passes that output to pipe3. Each pipe receives the previous pipe’s output as its first argument. This is identical to function composition: pipe3(pipe2(pipe1(value), arg)). Chaining keeps individual pipes simple and focused while enabling complex transformations.
Step 5 — Injectable Services in Pipes Enable Powerful Transformations
Pipes are classes decorated with @Pipe() and managed by Angular’s DI system. They can inject services through their constructor: constructor(private translateService: TranslateService) {}. This enables pipes that translate strings, format values according to user preferences (currency, date locale), or look up values from a cache. Services injected into pipes follow the same DI rules as services injected into components.
Common Mistakes
Mistake 1 — Multiple async pipe subscriptions for the same Observable
❌ Wrong — two separate HTTP requests are made:
<p>Total: {{ tasks$ | async | json }}</p>
<li *ngFor="let task of tasks$ | async">
<!-- Two subscriptions — two API calls! -->
✅ Correct — subscribe once with async + as:
<ng-container *ngIf="tasks$ | async as tasks">
<p>Total: {{ tasks.length }}</p>
<li *ngFor="let task of tasks">{{ task.title }}</li>
</ng-container>
Mistake 2 — Creating an impure pipe for list filtering
❌ Wrong — runs on every change detection cycle (many times per second):
@Pipe({ name: 'filterByStatus', pure: false }) // impure!
export class FilterByStatusPipe implements PipeTransform {
transform(tasks: Task[], status: string): Task[] {
return tasks.filter(t => t.status === status); // runs constantly
}
}
✅ Correct — compute filtered list in the component as a computed signal:
filteredTasks = computed(() =>
this.tasks().filter(t => t.status === this.filterStatus())
);
// Template: *ngFor="let t of filteredTasks()"
Mistake 3 — Not importing pipes in standalone components
❌ Wrong — DatePipe not available in template:
@Component({
standalone: true,
imports: [CommonModule], // DatePipe not imported!
template: `{{ date | date:'medium' }}`
})
// Error: The pipe 'date' could not be found
✅ Correct — import built-in pipes individually or via CommonModule:
@Component({
standalone: true,
imports: [DatePipe, CurrencyPipe, RelativeDatePipe], // explicit imports
template: `{{ date | date:'medium' }}`
})
Quick Reference
| Pipe | Usage | Example Output |
|---|---|---|
| date | now | date:'shortDate' |
1/15/25 |
| date (medium) | now | date:'mediumDate' |
Jan 15, 2025 |
| currency | price | currency:'USD' |
$99.90 |
| number | 3.14159 | number:'1.2-3' |
3.142 |
| percent | 0.75 | percent |
75% |
| uppercase | 'hello' | uppercase |
HELLO |
| titlecase | 'in progress' | titlecase |
In Progress |
| slice | tags | slice:0:3 |
First 3 tags |
| async | tasks$ | async |
Current value or null |
| async + as | *ngIf="tasks$ | async as tasks" |
Single subscription |
| json (debug) | obj | json |
Pretty JSON string |
| keyvalue | statsObj | keyvalue |
Array of {key, value} |