Skip to main content

πŸ”§ Pipes for Data Transformation

Raw data is rarely display-ready. A price is a bare number, a date is a timestamp, a name is lowercase. Angular pipes let you format that data right in the template β€” declaratively, reusably, and without cluttering your components.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Apply built-in pipes for text, numbers, dates, and currency in templates
  • Pass parameters to pipes and chain several together
  • Use the async pipe to render Observables without manual subscription
  • Write your own standalone custom pipe by implementing PipeTransform
  • Explain the difference between pure and impure pipes and its performance impact

Estimated Time: 30–40 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Build a fileSize pipe that turns raw byte counts into "3.29 MB".

In This Lesson

What Pipes Are

A pipe is a small, reusable function you apply inside a template to transform a value for display only. It takes a value in, returns a transformed value out, and never changes the underlying data. Common jobs: format a date, uppercase text, render a number as currency, or trim a long string.

πŸ“– Real-world analogy: kitchen tools

Pipes are like the tools that turn ingredients into something plate-ready. A colander drains pasta; a grater shreds cheese; a sieve smooths flour. The raw ingredient (your data) is untouched in the pantry β€” the tool just prepares a version fit to serve. Different data needs different pipes, just as different dishes need different tools.

Doing the same formatting in the component would scatter presentation logic across your TypeScript. Pipes keep that logic declarative and in one place β€” the template β€” where it belongs.

Pipe Syntax

You apply a pipe with the vertical bar |. The value on the left is fed into the pipe on the right:

{{ value | pipeName }}

Parameters follow a colon:

{{ value | pipeName:param1:param2 }}

And you can chain pipes β€” each receives the output of the previous:

{{ birthday | date:'longDate' | uppercase }}

One important detail with standalone components: a pipe must be imported before you can use it. Built-in pipes like DatePipe and CurrencyPipe live in @angular/common:

import { Component } from '@angular/core';
import { UpperCasePipe, CurrencyPipe, DatePipe } from '@angular/common';

@Component({
  selector: 'app-product',
  standalone: true,
  imports: [UpperCasePipe, CurrencyPipe, DatePipe],
  template: `
    <p>Name: {{ product.name | uppercase }}</p>
    <p>Price: {{ product.price | currency:'USD' }}</p>
    <p>Added: {{ product.addedOn | date:'medium' }}</p>
  `
})
export class ProductComponent {
  product = { name: 'headphones', price: 249.99, addedOn: new Date() };
}

⚠️ "Pipe not found" error?

If you forget the import, Angular throws The pipe 'currency' could not be found. This is the number-one pipe gotcha in standalone apps β€” the pipe exists, you just haven't added it to the component's imports array.

Built-in Pipes

Angular ships a rich set of pipes covering the most common formatting needs:

graph TD A[Angular Built-in Pipes] --> B["Text
uppercase Β· lowercase Β· titlecase"] A --> C["Numbers
number Β· percent Β· currency"] A --> D["Dates
date"] A --> E["Objects
json Β· keyvalue"] A --> F["Arrays / async
slice Β· async"]

Text pipes

{{ 'angular pipes' | uppercase }}         <!-- ANGULAR PIPES -->
{{ 'Angular Pipes' | lowercase }}         <!-- angular pipes -->
{{ 'angular pipes example' | titlecase }} <!-- Angular Pipes Example -->

Number, percent, and currency

<!-- number:'minIntDigits.minFraction-maxFraction' -->
{{ 1234.5678 | number }}          <!-- 1,234.568 -->
{{ 1234.5678 | number:'1.2-2' }}  <!-- 1,234.57 -->

{{ 0.8456 | percent }}            <!-- 85% -->
{{ 0.8456 | percent:'1.2-2' }}    <!-- 84.56% -->

{{ 49.99 | currency }}                 <!-- $49.99 -->
{{ 49.99 | currency:'EUR' }}           <!-- €49.99 -->
{{ 4999 | currency:'JPY':'code' }}     <!-- JPY 4,999 -->

The date pipe

One of the most-used pipes, with named formats and full custom patterns:

<!-- assume today = new Date() -->
{{ today | date:'fullDate' }}   <!-- Monday, June 15, 2026 -->
{{ today | date:'shortDate' }}  <!-- 6/15/26 -->
{{ today | date:'mediumDate' }} <!-- Jun 15, 2026 -->
{{ today | date:'shortTime' }}  <!-- 1:30 PM -->
{{ today | date:'EEEE, MMMM d, y, h:mm a' }} <!-- Monday, June 15, 2026, 1:30 PM -->
{{ today | date:'medium':'UTC' }}            <!-- with an explicit time zone -->

Object and array pipes

<!-- json is great for debugging -->
<pre>{{ user | json }}</pre>

<!-- keyvalue iterates an object's entries -->
@for (entry of profile | keyvalue; track entry.key) {
  <li>{{ entry.key }}: {{ entry.value }}</li>
}

<!-- slice takes a sub-range of an array (or string) -->
@for (fruit of fruits | slice:0:3; track fruit) {
  <span>{{ fruit }}</span>
}

πŸ’‘ Locale-aware by design

The date, number, currency, and percent pipes respect Angular's locale settings, so the same template renders $1,234.57 for a US user and 1.234,57 € for a German user once you configure locale data. That's a big reason to prefer them over hand-rolled formatting.

The async Pipe

The async pipe is special: it subscribes to an Observable or Promise, returns the latest value, and β€” critically β€” unsubscribes automatically when the component is destroyed. That single behavior eliminates a whole category of memory-leak bugs.

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { interval, map, take } from 'rxjs';

@Component({
  selector: 'app-async-example',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <h3>Count: {{ counter$ | async }}</h3>

    @if (user$ | async; as user) {
      <h4>Welcome, {{ user.name }}!</h4>
    } @else {
      <p>Loading user data…</p>
    }
  `
})
export class AsyncExampleComponent {
  counter$ = interval(1000).pipe(map(i => i + 1), take(10));

  user$ = new Promise<{ name: string }>(resolve =>
    setTimeout(() => resolve({ name: 'Ada Lovelace' }), 2000));
}

Note the as user syntax: it stashes the resolved value in a template variable so you can use it, and the @else block renders a loading state until the data arrives.

πŸ“– async pipe vs toSignal()

Both bring async values into the template and both clean up after themselves. Use the async pipe when you're staying in the Observable world; use toSignal() (from the previous lesson) when you'd rather work with a signal in your component class. They're two doors to the same room.

Writing a Custom Pipe

Built-ins cover the basics, but every app needs a few bespoke transforms. Creating one takes four steps: make a class, implement PipeTransform, decorate it with @Pipe, and import it where you use it (modern pipes are standalone: true).

Example: a file-size pipe

Turn a raw byte count into a friendly "3.29 MB":

// file-size.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'fileSize',
  standalone: true
})
export class FileSizePipe implements PipeTransform {
  transform(bytes: number, decimals = 2): string {
    if (!bytes) return '0 Bytes';

    const k = 1024;
    const units = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    const value = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals));

    return `${value} ${units[i]}`;
  }
}

Because it's standalone, you just import it into any component that needs it β€” no NgModule declaration:

import { Component } from '@angular/core';
import { FileSizePipe } from './file-size.pipe';

@Component({
  selector: 'app-files',
  standalone: true,
  imports: [FileSizePipe],
  template: `
    @for (file of files; track file.name) {
      <li>{{ file.name }} β€” {{ file.size | fileSize }}</li>
    }
  `
})
export class FilesComponent {
  files = [
    { name: 'Report.pdf',       size: 3_450_000 },     // 3.29 MB
    { name: 'Photos.zip',       size: 147_800_000 },   // 140.95 MB
    { name: 'Config.json',      size: 1_230 },         // 1.2 KB
    { name: 'Backup.iso',       size: 4_700_000_000 }  // 4.38 GB
  ];
}

Renders as:

Report.pdf β€” 3.29 MB
Photos.zip β€” 140.95 MB
Config.json β€” 1.2 KB
Backup.iso β€” 4.38 GB

The transform method is the whole contract: first argument is the piped value, any further arguments are the parameters after each colon (| fileSize:0 would pass decimals = 0).

Pure vs Impure Pipes

Every pipe is pure by default, and that word has a precise meaning: Angular only re-runs a pure pipe when its input changes by reference. That makes pure pipes fast, because they run rarely. An impure pipe re-runs on every change-detection cycle β€” powerful, but potentially a performance trap.

graph TB A[Pipes] --> B["Pure (default)"] A --> C[Impure] B --> D["Re-runs only when input
changes by reference"] C --> E["Re-runs every change
detection cycle"] D --> F["Fast β€” the safe default"] E --> G["Sees changes inside
objects & arrays"]

The catch with pure pipes: if you mutate an array in place (say, this.items.push(x)), the reference is unchanged, so a pure pipe over that array won't re-run. The fix is to replace the reference (this.items = [...this.items, x]) β€” which is exactly what signals encourage anyway.

@Pipe({
  name: 'liveFilter',
  standalone: true,
  pure: false   // re-evaluates on every change detection cycle
})
export class LiveFilterPipe implements PipeTransform {
  transform(items: string[], term: string): string[] {
    return items.filter(i => i.toLowerCase().includes(term.toLowerCase()));
  }
}

⚠️ Don't filter big lists with an impure pipe

An impure filtering pipe over a large array re-runs on every keystroke, mouse move, and timer tick β€” quietly tanking performance. The Angular team's own guidance: do filtering and sorting in the component (or a computed signal), not in a pipe. Reserve impure pipes for genuinely small, dynamic cases like the built-in async pipe.

Hands-on Exercise

πŸ‹οΈ Build a truncate Pipe

Objective: Write a parameterized custom pipe that shortens long text and appends a suffix.

Requirements:

  1. Create a standalone TruncatePipe with name: 'truncate'.
  2. Accept a limit (default 20) and a suffix (default '…').
  3. Return the text unchanged if it's already at or under the limit.
  4. Handle null/undefined input gracefully (return an empty string).
  5. Use it in a template: {{ post.body | truncate:50 }}.
πŸ’‘ Hint

Guard the empty case first (if (!value) return ''), then compare value.length against limit. When you do truncate, slice(0, limit) and append the suffix. Keep it a pure pipe β€” text transforms are the textbook pure case.

βœ… Sample solution
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string | null | undefined, limit = 20, suffix = '…'): string {
    if (!value) return '';
    if (value.length <= limit) return value;
    return value.slice(0, limit).trimEnd() + suffix;
  }
}

// usage
import { Component } from '@angular/core';
import { TruncatePipe } from './truncate.pipe';

@Component({
  selector: 'app-feed',
  standalone: true,
  imports: [TruncatePipe],
  template: `
    @for (post of posts; track post.id) {
      <p>{{ post.body | truncate:50 }}</p>
    }
  `
})
export class FeedComponent {
  posts = [
    { id: 1, body: 'Angular pipes keep your templates clean and expressive.' },
    { id: 2, body: 'Short one.' }
  ];
}

🎯 Quick Quiz

Question 1: What is the correct syntax to format price as euros in a template?

Question 2: Which interface must a custom pipe class implement?

Question 3: Why should you avoid an impure pipe for filtering a large array?

Best Practices

βœ… Do

  • Prefer built-in pipes β€” they're tested, locale-aware, and free.
  • Keep each pipe focused on one transformation and make it generic enough to reuse.
  • Handle edge cases β€” always guard null/undefined input.
  • Keep pipes pure unless you have a specific, small reason not to.
  • Chain pipes for readable compound formatting (| date:'longDate' | uppercase).

⚠️ Avoid

  • Filtering/sorting large arrays in pipes β€” do it in the component or a computed signal.
  • Side effects in a pipe β€” no API calls, no DOM changes; a pipe is pure formatting.
  • Forgetting the import in standalone components β€” the classic "pipe not found" error.
  • Mutating arrays in place when a pure pipe watches them β€” replace the reference instead.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Pipes transform data for display in the template using the | syntax β€” the source data is untouched.
  • Built-in pipes cover text, numbers, dates, currency, objects, and arrays, and are locale-aware.
  • The async pipe subscribes and unsubscribes for you β€” no memory leaks.
  • Write a custom pipe by implementing PipeTransform and marking it standalone: true.
  • Pure pipes (default) re-run only on reference change and are fast; impure pipes run every cycle β€” use with care.
  • For heavy filtering/sorting, prefer the component or a computed signal over a pipe.

πŸ“š Further Reading

πŸš€ What's Next?

You've now covered Angular's core building blocks β€” components, communication, services, and pipes. Next we'll step back and compare the big three frameworks side by side: React vs Vue vs Angular, so you can reason about when to reach for each.

πŸŽ‰ Great work!

Your data now arrives on screen clean and readable. Time to zoom out and compare frameworks.