Creating Custom Pipes
Angular’s built-in pipes cover formatting essentials like dates, currency, and JSON, but real applications inevitably need domain-specific transformations: truncating text, formatting phone numbers, filtering arrays, or rendering business-specific units. Custom pipes let you encapsulate that logic in a small, declarative, reusable class that plugs straight into your templates. A custom pipe is just a class decorated with @Pipe that implements the PipeTransform interface, and in modern Angular it is standalone by default.
Anatomy of a custom pipe
Every custom pipe needs two things: the @Pipe decorator that registers a template name, and a transform method that receives the input value plus any arguments and returns the transformed result. The PipeTransform interface enforces the correct method signature so TypeScript catches mistakes early.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 25, trail = '…'): string {
if (!value) return '';
return value.length > limit ? value.substring(0, limit) + trail : value;
}
}
The name property is what you reference in templates with the | operator. The first parameter of transform is always the piped value; every parameter after that maps to a colon-separated argument in the template.
Pipes are standalone by default in Angular 17+. There is no
standalone: trueflag to set and noNgModuledeclaration — you simply import the pipe class wherever you use it.
Registering and using the pipe
Because the pipe is standalone, you register it by adding the class to a component’s imports array. Then it is available in that component’s template like any built-in pipe.
import { Component } from '@angular/core';
import { TruncatePipe } from './truncate.pipe';
@Component({
selector: 'app-article-card',
imports: [TruncatePipe],
template: `
<h3>{{ title }}</h3>
<p>{{ body | truncate }}</p>
<p>{{ body | truncate: 10 }}</p>
<p>{{ body | truncate: 10 : ' [more]' }}</p>
`,
})
export class ArticleCardComponent {
title = 'Working with Pipes';
body = 'Angular pipes transform data directly in the template.';
}
Output:
Working with Pipes
Angular pipes transform da…
Angular pi…
Angular pi [more]
Notice how arguments are passed positionally, separated by colons: truncate: 10 : ' [more]'. The first colon maps to limit, the second to trail. Omitted arguments fall back to the defaults declared in transform.
Passing dynamic arguments
Pipe arguments are not limited to literals — you can bind any component property or expression, and the pipe re-runs whenever the bound input or argument changes (for pure pipes, which is the default).
@Component({
selector: 'app-summary',
imports: [TruncatePipe],
template: `
<input type="range" min="5" max="50" [(ngModel)]="maxLength" />
<p>{{ description | truncate: maxLength }}</p>
`,
})
export class SummaryComponent {
description = 'Custom pipes keep transformation logic out of components.';
maxLength = 20;
}
As the user drags the slider, maxLength updates and the pipe re-evaluates automatically, producing a live-truncated string with no manual subscription or change handler.
Generating a pipe with the CLI
Rather than hand-writing boilerplate, scaffold a pipe with the Angular CLI. It creates the class, applies the @Pipe decorator, and adds a spec file for tests.
ng generate pipe truncate
This produces truncate.pipe.ts and truncate.pipe.spec.ts ready for you to fill in the transform body.
Typing the transform method
For robust pipes, type both the input and the return value precisely. Generics are useful when a pipe operates over collections so callers retain the element type.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'filterBy' })
export class FilterByPipe implements PipeTransform {
transform<T>(items: T[], predicate: (item: T) => boolean): T[] {
if (!Array.isArray(items)) return items;
return items.filter(predicate);
}
}
| Decorator option | Type | Purpose |
|---|---|---|
name | string | The identifier used in templates (required) |
pure | boolean | Whether the pipe is pure; defaults to true |
standalone | boolean | Standalone usage; defaults to true in v17+ |
Avoid building array-filtering or sorting pipes for production lists. They are convenient but couple presentation to data shaping; prefer transforming arrays with signals or a computed value in the component for predictable performance.
Best practices
- Implement
PipeTransformexplicitly so the compiler verifies yourtransformsignature. - Keep pipes pure and side-effect-free — return a new value, never mutate the input.
- Provide sensible default argument values so the pipe works with the bare
| namesyntax. - Type the input and return values precisely, using generics for collection pipes.
- Scaffold with
ng generate pipeto get a consistent class plus test file. - Reserve impure pipes for genuinely dynamic data and document why a pipe is impure.
- Favor a single focused responsibility per pipe rather than one pipe with many modes.