A pipe transforms a value directly in the template's display syntax — Angular ships several built-in ones (date, currency, uppercase), and writing a custom one follows the same pattern for any transformation that isn't already covered.
Using a built-in pipe
{{ order.createdAt | date:'medium' }}
{{ product.price | currency:'USD' }}
{{ user.name | uppercase }}
Generating a custom pipe with the CLI
ng generate pipe truncate
A basic custom pipe
@Pipe({
name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50): string {
if (value.length <= limit) return value;
return value.slice(0, limit) + '...';
}
}
{{ post.excerpt | truncate:100 }}
The transform() method's first parameter is always the value being piped (whatever appears to the left of the |), and any arguments after a colon in the template (:100 here) map to its subsequent parameters — the same pattern every built-in pipe follows.
A pipe with multiple parameters
@Pipe({
name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50, suffix: string = '...'): string {
if (value.length <= limit) return value;
return value.slice(0, limit) + suffix;
}
}
{{ post.excerpt | truncate:100:' [more]' }}
Chaining multiple pipes together
{{ post.title | truncate:30 | uppercase }}
Pipes chain left to right — each one receives the output of the previous one as its input, so the order they're listed in genuinely matters for the final result.
Why pipes must be pure by default
@Pipe({
name: 'truncate',
pure: true // this is the default
})
A pure pipe's transform() re-runs only when its actual input value or arguments change (by reference, for objects and arrays) — this is a deliberate performance optimization, since Angular would otherwise need to re-run every pipe on every single change detection cycle regardless of whether its inputs actually changed.
Creating an impure pipe (used sparingly)
@Pipe({
name: 'filterActive',
pure: false
})
export class FilterActivePipe implements PipeTransform {
transform(items: Item[]): Item[] {
return items.filter(item => item.isActive);
}
}
Marking a pipe impure makes it re-run on every change detection cycle, even if its input array reference hasn't changed — necessary if the pipe needs to react to mutations inside an array rather than reassignments of the array itself, but genuinely costly for performance if overused, since it defeats the whole point of the pure-by-default optimization.
Why a pipe, rather than just a component method
A component method bound in the template ({{ truncate(post.excerpt) }}) would actually re-run on every single change detection cycle regardless of purity — a pure pipe's built-in memoization is specifically what a plain method call in a template binding doesn't get for free, which is the real practical reason to reach for a pipe over a method for this kind of transformation.
Modern Angular generates standalone pipes by default
The CLI's current pipe generator creates a standalone pipe unless configured otherwise. A standalone component that uses the custom pipe imports the pipe class directly:
import { Component } from '@angular/core';
import { TruncatePipe } from './truncate-pipe';
@Component({
selector: 'app-post-card',
imports: [TruncatePipe],
template: `{{ post.excerpt | truncate:100 }}`,
})
export class PostCardComponent {}
Pure pipes react to reference changes, not arbitrary mutation
For an array input, items.push(newItem) keeps the same array reference, so a pure pipe may not run again. Creating a new array reference makes the change visible:
this.items = [...this.items, newItem];
This is usually a better fix than marking a filtering pipe impure. Immutable-style updates preserve pure-pipe performance and make change detection easier to reason about.
Do not use pipes for side effects
A pipe should compute a display value from its inputs. It should not write to a service, mutate the incoming object, start network requests, or depend on hidden mutable state. Angular may invoke template transformations in ways that are inconvenient for side effects; keeping transform() deterministic makes both rendering and tests predictable.
Heavy data filtering often belongs before the template
Even a pure pipe is not automatically the right home for expensive sorting or filtering over a large collection. If the transformation is domain logic or feeds more than presentation, compute it in component state, a signal/computed value, or a reusable function and give the template the already-prepared result. Pipes are strongest when they make template presentation concise, not when they hide a data-processing pipeline.