| |

Angular 73 🅰️ Custom Pipes in Depth

A pipe is a special operator in Angular template expressions that allows you to transform data declaratively. The vertical bar character (|) is the pipe operator, inspired by the Unix pipe, and it takes a value on the left and passes it through a transformation function on the right . Angular ships with built-in pipes for common tasks — formatting dates, currency, uppercase, lowercase, percent, JSON debugging, and slicing arrays. When those built-ins don’t cover your needs, you write a custom pipe.

Custom pipes are the extension mechanism of Angular templates. They encapsulate a reusable data transformation — reversing a string, truncating text, filtering a list, formatting a phone number — and make it available anywhere in the application with a single vertical bar. The Angular CLI scaffolds a pipe with ng generate pipe , giving you a class with the @Pipe decorator and a transform method that you fill in with your logic.

Key point: Every pipe is a class that implements the PipeTransform interface and is decorated with @Pipe. The name property in the decorator is what you use in the template. The transform method receives the input value on the left of the pipe operator, and any parameters after the colon (:) on the right . Pipes are pure by default, meaning Angular only re-executes the transform when the input reference changes. Setting pure: false makes the pipe impure, which forces it to run on every change detection cycle — a powerful but dangerous option .


Why custom pipes exist

Angular’s built-in pipes cover formatting: dates, numbers, currency, text case. They do not cover domain-specific transformations. An application that displays product codes, order statuses, or user roles needs transformations that Angular cannot anticipate.

The repetition problem. A formatOrderStatus function that maps 'PENDING' to 'Awaiting processing' and 'SHIPPED' to 'In transit' will be needed in the order list, the order detail page, the admin dashboard, and the email template. Without a pipe, you either duplicate the function or call it from every component’s TypeScript. A pipe declares the transformation once and makes it available in any template .

The declarative problem. Calling formatStatus(order.status) in a template works, but it hides the transformation. Reading {{ order.status | orderStatus }} makes the intent explicit: this value is being transformed by a named pipe. The template stays declarative, and the transformation is visible to anyone reading the markup.

The state problem. Some transformations depend on state outside their arguments — a language setting, a user preference, a timezone. A pure pipe cannot see that state unless it’s passed as a parameter. An impure pipe can inject a service and read current state on every cycle, at the cost of performance .

The trade-off. Pipes are pure by default for a reason: Angular’s change detection runs frequently, and a pipe that recomputes an expensive transformation on every cycle will degrade performance. Impure pipes exist for the cases where the transformation genuinely depends on mutable state, but they should be a deliberate choice, not a default .


a. Creating a Custom Pipe

A custom pipe is a TypeScript class with the @Pipe decorator and a transform method. The CLI generates the boilerplate: ng generate pipe reverse creates reverse.pipe.ts and reverse.pipe.spec.ts . In modern Angular, pipes are standalone by default, meaning they are imported directly into the components that use them rather than declared in an NgModule .

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'reverse',
  standalone: true,
})
export class ReversePipe implements PipeTransform {
  transform(value: string): string {
    let reversed = '';
    for (let i = value.length - 1; i >= 0; i--) {
      reversed += value[i];
    }
    return reversed;
  }
}

The name property is the string you use in the template. The transform method receives the value on the left of the pipe operator. For {{ word | reverse }}, word is passed as value, and the return value is rendered .

The pipe must be imported in the component’s imports array:

@Component({
  selector: 'app-root',
  imports: [ReversePipe],
  template: `<p>Reversed: {{ word | reverse }}</p>`,
})
export class AppComponent {
  word = 'hello';
}

Without the import, Angular does not know the pipe exists and the template fails to compile. This is the most common mistake when adopting standalone pipes .

The transform method can accept additional parameters after the input value. These are specified in the template with colons:

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50, trail: string = '...'): string {
    return value.length > limit ? value.substring(0, limit) + trail : value;
  }
}

Usage: {{ longText | truncate:100:'…' }} truncates to 100 characters and appends an ellipsis . The parameters are positional and optional. The limit parameter defaults to 50, and the trail parameter defaults to '...' if not provided.


b. Pure Pipes vs Impure Pipes

Every pipe has a pure property in its decorator. The default is true. A pure pipe is executed only when Angular detects a pure change to the input value — a change in the reference of the input, not a mutation of its contents .

@Pipe({ name: 'filterActive', standalone: true, pure: true })
export class FilterActivePipe implements PipeTransform {
  transform(items: Item[]): Item[] {
    return items.filter(item => item.active);
  }
}

If the parent component passes items and later calls items.push(newItem), the pipe does not re-execute. The array reference is the same, so Angular sees no pure change. To trigger the pipe, the parent must create a new array: items = [...items, newItem] .

An impure pipe runs on every change detection cycle, regardless of whether the input reference changed. This is set with pure: false .

@Pipe({ name: 'filterActive', standalone: true, pure: false })
export class FilterActivePipe implements PipeTransform {
  transform(items: Item[]): Item[] {
    return items.filter(item => item.active);
  }
}

Now items.push() triggers re-execution because the pipe runs on every cycle. But this comes at a cost. The pipe runs on every keystroke, every mouse move, every timer tick — every time Angular checks for changes. An expensive transformation in an impure pipe will destroy performance .

The AsyncPipe is the canonical impure pipe. It subscribes to an observable or promise, and when the source emits a new value, the pipe marks the component for check and returns the latest value. It must be impure because the input observable’s reference does not change — only the value it emits does .

The rule is straightforward: use pure pipes unless you have a specific reason not to. If a transformation depends on mutable state that the pipe cannot see through its arguments, consider whether the state can be passed as a parameter or converted to a new reference. If it genuinely cannot, an impure pipe is the answer, but budget the performance cost consciously .


c. Pipes with Services and Async Transformations

A pipe can inject services through its constructor. This lets the pipe use application services — a translation service, a configuration service, a formatting service — to perform its transformation.

@Pipe({ name: 'translate', standalone: true })
export class TranslatePipe implements PipeTransform {
  private i18n = inject(TranslationService);

  transform(key: string): string {
    return this.i18n.get(key);
  }
}

The pipe is pure, so it only re-executes when the key changes. If the language changes, the pipe does not know. The component must pass the current language as a parameter, or the pipe must be impure .

A more complex pattern involves returning an observable from a pipe and chaining the async pipe. This is the approach when the transformation depends on an asynchronous source:

@Pipe({ name: 'utcToText', standalone: true })
export class UtcToTextPipe implements PipeTransform {
  private timezoneService = inject(TimeZoneService);

  transform(timezone: string): Observable<string> {
    return this.timezoneService.getTimeZones().pipe(
      map(zones => zones.find(z => z.utc.includes(timezone))?.text ?? timezone)
    );
  }
}

Template: {{ subscription.timeZone | utcToText | async }} . The pipe returns an observable, and async subscribes to it. This pattern keeps the pipe pure — it re-executes only when the input timezone changes — while still performing an asynchronous transformation.

A pipe that subscribes to an observable internally must handle unsubscription. If the pipe is impure and subscribes in transform, it leaks. The AsyncPipe handles this with ngOnDestroy . A custom pipe that needs lifecycle cleanup should implement OnDestroy and unsubscribe .


Complete Example Session

This session builds a pure truncate pipe, an impure filter pipe, a pipe with parameters, a pipe that uses a service, and an async pipe pattern.

// ============================================
// PART 1: THE BASIC PURE PIPE
// ============================================

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'reverse',
  standalone: true,
})
export class ReversePipe implements PipeTransform {
  transform(value: string): string {
    return value.split('').reverse().join('');
  }
}

// Template: {{ 'hello' | reverse }} → "olleh"

// ============================================
// PART 2: THE PIPE WITH PARAMETERS
// ============================================

@Pipe({
  name: 'truncate',
  standalone: true,
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50, trail: string = '…'): string {
    if (!value) return '';
    return value.length > limit ? value.substring(0, limit) + trail : value;
  }
}

// Template: {{ longText | truncate:100:'...' }}

// ============================================
// PART 3: THE PURE FILTER PIPE
// ============================================

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

@Pipe({
  name: 'completedTasks',
  standalone: true,
  pure: true, // default
})
export class CompletedTasksPipe implements PipeTransform {
  transform(tasks: Task[]): Task[] {
    return tasks.filter(task => task.completed);
  }
}

// Template: {{ tasks | completedTasks }}

// ============================================
// PART 4: THE IMPURE FILTER PIPE
// ============================================

@Pipe({
  name: 'activeTasks',
  standalone: true,
  pure: false, // runs on every change detection
})
export class ActiveTasksPipe implements PipeTransform {
  transform(tasks: Task[]): Task[] {
    return tasks.filter(task => !task.completed);
  }
}

// This pipe re-executes even when the array reference is unchanged.
// Useful when the parent mutates the array in place.

// ============================================
// PART 5: THE PIPE WITH A SERVICE
// ============================================

import { inject } from '@angular/core';

@Pipe({
  name: 'currencyName',
  standalone: true,
})
export class CurrencyNamePipe implements PipeTransform {
  private currencyService = inject(CurrencyService);

  transform(code: string): string {
    return this.currencyService.getName(code) ?? code;
  }
}

// Template: {{ order.currencyCode | currencyName }}

// ============================================
// PART 6: THE ASYNC PIPE PATTERN
// ============================================

import { Pipe, PipeTransform } from '@angular/core';
import { Observable, map } from 'rxjs';

@Pipe({
  name: 'userName',
  standalone: true,
})
export class UserNamePipe implements PipeTransform {
  private userService = inject(UserService);

  transform(userId: string): Observable<string> {
    return this.userService.getUser(userId).pipe(
      map(user => user.displayName)
    );
  }
}

// Template: {{ order.userId | userName | async }}

// ============================================
// PART 7: THE PIPE WITH ON DESTROY
// ============================================

import { Pipe, PipeTransform, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

@Pipe({
  name: 'subscribeTo',
  standalone: true,
  pure: false,
})
export class SubscribeToPipe implements PipeTransform, OnDestroy {
  private subscription = new Subscription();
  private latestValue: unknown = null;

  transform(source: Observable<unknown> | null): unknown {
    if (!source) return null;
    this.subscription.unsubscribe();
    this.subscription = source.subscribe(value => {
      this.latestValue = value;
    });
    return this.latestValue;
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

// Custom async pipe with explicit cleanup.

// ============================================
// PART 8: THE CHAINED PIPES
// ============================================

// Pipes can be chained:
// {{ birthday | date:'fullDate' | uppercase }}

// Angular runs them left to right:
// 1. date:'fullDate' → "Monday, May 28, 2018"
// 2. uppercase → "MONDAY, MAY 28, 2018"

// ============================================
// PART 9: THE PIPE IN A STANDALONE COMPONENT
// ============================================

import { Component } from '@angular/core';

@Component({
  selector: 'app-task-list',
  standalone: true,
  imports: [CompletedTasksPipe, TruncatePipe],
  template: `
    <ul>
      @for (task of tasks | completedTasks; track task.id) {
        <li>{{ task.title | truncate:30 }}</li>
      }
    </ul>
  `,
})
export class TaskListComponent {
  tasks: Task[] = [
    { id: 1, title: 'Write documentation', completed: true },
    { id: 2, title: 'Review pull request', completed: false },
  ];
}

// ============================================
// PART 10: THE NULL SAFETY PIPE
// ============================================

@Pipe({
  name: 'safe',
  standalone: true,
})
export class SafePipe implements PipeTransform {
  transform(value: string | null | undefined, fallback: string = '—'): string {
    return value ?? fallback;
  }
}

// Template: {{ user.middleName | safe:'N/A' }}
// Handles null and undefined gracefully.

The ten parts cover the basic pure pipe, the pipe with parameters, the pure filter pipe, the impure filter pipe, the pipe with a service, the async pipe pattern, the pipe with OnDestroy, chained pipes, standalone component usage, and null safety.


Quick Reference

The Pipe Anatomy

PartPurpose
@Pipe decoratorDeclares the class as a pipe
nameThe string used in templates
puretrue (default) or false (impure)
standaloneWhether the pipe is standalone
transformThe method that performs the transformation

The Pipe Syntax

SyntaxMeaning
{{ value | pipeName }}Apply pipe to value
{{ value | pipeName:arg1:arg2 }}Pass parameters
{{ value | pipe1 | pipe2 }}Chain pipes left to right

The Pure vs Impure

PropertyPure (true)Impure (false)
RunsOnly on input reference changeEvery change detection cycle
PerformanceFastPotentially slow
Use caseMost transformationsAsync sources, mutable state
ExampleDatePipe, UpperCasePipeAsyncPipe

The Common Built-in Pipes

PipePurpose
dateFormat dates by locale
currencyFormat numbers as currency
numberFormat numbers with decimals
percentFormat numbers as percentages
uppercase / lowercaseChange text case
titlecaseTitle-case text
jsonDebug object as JSON
asyncSubscribe to Observable/Promise
sliceSubset of array or string
keyvalueObject to key-value pairs

Best Practices

✅ Do This:

// Use pure pipes by default
@Pipe({ name: 'truncate', standalone: true })               // ✅
// Handle null and undefined inputs gracefully
transform(value: string | null): string {
  return value ?? '';                                        // ✅
}
// Use parameter defaults
transform(value: string, limit: number = 50): string { ... } // ✅
// Import standalone pipes in component imports
imports: [TruncatePipe]                                      // ✅
// Use async pipe for observable transformations
{{ data$ | async }}                                          // ✅

❌ Don’t Do This:

// Don't use impure pipes for static transformations
@Pipe({ name: 'format', pure: false })                       // ❌
// Don't ignore null safety
transform(value: string): string { return value.toUpperCase(); } // ❌ crashes on null
// Don't forget to import standalone pipes
@Component({ imports: [] })                                 // ❌ pipe not found
// Don't subscribe to observables in impure pipes without cleanup
transform(obs$: Observable<any>) { obs$.subscribe(); }       // ❌ memory leak

Common Pitfalls

PitfallWhy It HappensFix
Pipe doesn’t re-runPure pipe with mutated inputReturn new reference or use pure: false
Performance degradationImpure pipe with expensive logicUse pure pipe with new references
Pipe not foundStandalone pipe not importedAdd to component imports
Null crashMissing null checkUse value ?? fallback
Chained pipe order wrongAssuming right-to-leftAngular runs left to right
Pipe and async don’t workPipe returns value, not observableReturn Observable<T> and chain async

Real-World Examples

1. Reverse String

@Pipe({ name: 'reverse', standalone: true })
export class ReversePipe implements PipeTransform {
  transform(value: string): string {
    return value.split('').reverse().join('');
  }
}

2. Truncate with Parameters

transform(value: string, limit: number = 50): string {
  return value.length > limit ? value.substring(0, limit) + '…' : value;
}

3. Filter Completed Tasks (Pure)

transform(tasks: Task[]): Task[] {
  return tasks.filter(t => t.completed);
}

4. Filter Active Tasks (Impure)

@Pipe({ name: 'activeTasks', pure: false })

5. Currency Name from Service

transform(code: string): string {
  return this.currencyService.getName(code) ?? code;
}

6. Async User Name

transform(userId: string): Observable<string> {
  return this.userService.getUser(userId).pipe(map(u => u.displayName));
}

7. Chained Date and Uppercase

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

8. Null-Safe Display

transform(value: string | null, fallback: string = '—'): string {
  return value ?? fallback;
}

9. Standalone Import

@Component({ imports: [ReversePipe, TruncatePipe] })

10. Pipe with OnDestroy

ngOnDestroy() { this.subscription.unsubscribe(); }

Visual: Pipe Anatomy

┌──────────────────────────────────────────────┐
│  PIPE ANATOMY                                │
│                                              │
│  @Pipe({                                     │
│    name: 'truncate',      ← template name    │
│    standalone: true,      ← importable       │
│    pure: true,            ← default          │
│  })                                          │
│  export class TruncatePipe                   │
│    implements PipeTransform {                │
│      transform(           ← logic goes here  │
│        value: string,                        │
│        limit: number = 50                    │
│      ): string { ... }                       │
│    }                                         │
│                                              │
│  Template: {{ text | truncate:100 }}         │
│                                              │
└──────────────────────────────────────────────┘

Visual: Pure vs Impure Execution

┌──────────────────────────────────────────────┐
│  PURE PIPE                                   │
│                                              │
│  Change detection cycle 1:                   │
│    input ref: A → RUN                        │
│    result: X                                 │
│                                              │
│  Change detection cycle 2:                   │
│    input ref: A (same) → CACHED (return X)   │
│                                              │
│  Change detection cycle 3:                   │
│    input ref: B (new) → RUN                  │
│                                              │
│  Fast. Runs only when reference changes.     │
│                                              │
├──────────────────────────────────────────────┤
│  IMPURE PIPE                                 │
│                                              │
│  Change detection cycle 1:                   │
│    input ref: A → RUN                        │
│                                              │
│  Change detection cycle 2:                   │
│    input ref: A (same) → RUN AGAIN           │
│                                              │
│  Change detection cycle 3:                   │
│    input ref: A (same) → RUN AGAIN           │
│                                              │
│  Slow. Runs every cycle.                     │
│                                              │
└──────────────────────────────────────────────┘

Visual: Pipe Chaining

┌──────────────────────────────────────────────┐
│  PIPE CHAINING                               │
│                                              │
│  {{ birthday | date:'fullDate' | uppercase }}│
│                                              │
│  birthday                                    │
│    │                                         │
│    ▼                                         │
│  date:'fullDate' → "Monday, May 28, 2018"    │
│    │                                         │
│    ▼                                         │
│  uppercase → "MONDAY, MAY 28, 2018"          │
│                                              │
│  Left to right. Each pipe receives           │
│  the previous pipe's output.                 │
│                                              │
└──────────────────────────────────────────────┘

Visual: Pipe with Service and Async

┌──────────────────────────────────────────────┐
│  PIPE + SERVICE + ASYNC                      │
│                                              │
│  Pipe:                                       │
│  transform(id: string): Observable<string>   │
│    └─ this.service.getUser(id).pipe(         │
│         map(u => u.displayName)              │
│       )                                      │
│                                              │
│  Template:                                   │
│  {{ order.userId | userName | async }}       │
│                                              │
│  userName returns Observable<string>         │
│  async subscribes and renders the value      │
│                                              │
│  The pipe stays pure. Async handles          │
│  the subscription.                           │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Pipe decorator@Pipe({ name, standalone, pure })
InterfacePipeTransform with transform method
Template syntax{{ value | pipeName:arg }}
Pure (default)Runs on reference change only
ImpureRuns every change detection cycle
StandaloneDefault in modern Angular, import in component
ParametersColon-separated after pipe name
ChainingLeft to right
Async pipeCanonical impure pipe
Null safetyHandle null/undefined in transform

Key takeaways:

  • A pipe is a class with @Pipe that implements PipeTransform. The name is used in templates. The transform method receives the value and optional parameters. The return value is rendered .
  • Pipes are pure by default. A pure pipe runs only when the input reference changes. Mutating an array or object in place does not trigger re-execution. Return a new reference to trigger the pipe .
  • Impure pipes run on every change detection cycle. This is necessary for pipes like AsyncPipe that subscribe to observables. It is expensive and should be used deliberately, not by default .
  • Standalone pipes must be imported. In modern Angular, pipes are standalone by default. They are added to the component’s imports array, not declared in an NgModule .
  • Pipes accept parameters with colon syntax. {{ value | truncate:100:'…' }} passes 100 and '…' as arguments to the transform method after the input value .
  • Pipes can chain. Angular runs them left to right. {{ date | date:'short' | uppercase }} formats then uppercases .
  • Pipes can inject services. A pipe with a constructor or inject can use application services. If the transformation depends on mutable service state, the pipe must be impure or the state must be passed as a parameter .
  • The async pattern keeps pipes pure. A pipe that returns an Observable and is chained with async performs asynchronous transformations without becoming impure .

Remember: Custom pipes are the declarative transformation layer of Angular templates. They encapsulate reusable logic — reversing, truncating, filtering, formatting — behind a single vertical bar. Pure pipes are the default and the right choice for most transformations: they run only when the input reference changes, which keeps change detection fast. Impure pipes are for the cases where the transformation depends on mutable state that the pipe cannot see through its arguments, and they should be used sparingly because they run on every cycle. Standalone pipes are imported directly into components, not declared in modules. Handle null inputs, keep the transform method free of side effects, and chain pipes when multiple transformations are needed. A well-written pipe is a small, focused function that makes templates cleaner and logic reusable.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!