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
| Part | Purpose |
|---|---|
@Pipe decorator | Declares the class as a pipe |
name | The string used in templates |
pure | true (default) or false (impure) |
standalone | Whether the pipe is standalone |
transform | The method that performs the transformation |
The Pipe Syntax
| Syntax | Meaning |
|---|---|
{{ value | pipeName }} | Apply pipe to value |
{{ value | pipeName:arg1:arg2 }} | Pass parameters |
{{ value | pipe1 | pipe2 }} | Chain pipes left to right |
The Pure vs Impure
| Property | Pure (true) | Impure (false) |
|---|---|---|
| Runs | Only on input reference change | Every change detection cycle |
| Performance | Fast | Potentially slow |
| Use case | Most transformations | Async sources, mutable state |
| Example | DatePipe, UpperCasePipe | AsyncPipe |
The Common Built-in Pipes
| Pipe | Purpose |
|---|---|
date | Format dates by locale |
currency | Format numbers as currency |
number | Format numbers with decimals |
percent | Format numbers as percentages |
uppercase / lowercase | Change text case |
titlecase | Title-case text |
json | Debug object as JSON |
async | Subscribe to Observable/Promise |
slice | Subset of array or string |
keyvalue | Object 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Pipe doesn’t re-run | Pure pipe with mutated input | Return new reference or use pure: false |
| Performance degradation | Impure pipe with expensive logic | Use pure pipe with new references |
| Pipe not found | Standalone pipe not imported | Add to component imports |
| Null crash | Missing null check | Use value ?? fallback |
| Chained pipe order wrong | Assuming right-to-left | Angular runs left to right |
| Pipe and async don’t work | Pipe returns value, not observable | Return 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
| Item | Value |
|---|---|
| Pipe decorator | @Pipe({ name, standalone, pure }) |
| Interface | PipeTransform with transform method |
| Template syntax | {{ value | pipeName:arg }} |
| Pure (default) | Runs on reference change only |
| Impure | Runs every change detection cycle |
| Standalone | Default in modern Angular, import in component |
| Parameters | Colon-separated after pipe name |
| Chaining | Left to right |
| Async pipe | Canonical impure pipe |
| Null safety | Handle null/undefined in transform |
Key takeaways:
- A pipe is a class with
@Pipethat implementsPipeTransform. Thenameis used in templates. Thetransformmethod 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
AsyncPipethat 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
importsarray, not declared in an NgModule . - Pipes accept parameters with colon syntax.
{{ value | truncate:100:'…' }}passes100and'…'as arguments to thetransformmethod 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
injectcan 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
Observableand is chained withasyncperforms 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!