Skip to main content

Pipes in Angular (built-in and custom)

Pipes in Angular transform data in templates: a value passes through a transform() function and comes out formatted for display. The component property stays unchanged.

Theory

TL;DR

  • A pipe is like a coffee filter: raw data goes in, formatted data comes out
  • Built-in pipes (date, currency, uppercase) handle common formatting without extra code
  • Custom pipes are classes with a transform() method, registered with @Pipe
  • Chain pipes with |: {{ value | pipe1 | pipe2 }}, each output feeds the next input
  • Pipes are pure by default: Angular only reruns them when the input reference changes

Quick example

html
<!-- Built-in pipes --> {{ today | date:'short' }} <!-- 1/15/26, 2:30 PM --> {{ price | currency:'USD' }} <!-- $1,234.50 --> {{ 'hello world' | uppercase }} <!-- HELLO WORLD --> <!-- Chained: format as currency, then slice the string --> {{ price | currency:'USD' | slice:0:7 }} <!-- $1,234. -->

Pipes do not touch the original value. The component property stays as-is; the pipe only changes what appears in the template.

Display vs. logic

Pipes format data for display, not for business logic. If you need the transformed value in TypeScript code (to send to an API, store in state, or use in a calculation), transform it in the component or service instead. Formatting a phone number for display is a good pipe use case. Calculating an annual salary with tax rates applied is not.

Built-in pipes

Angular ships with pipes for the most common formatting tasks:

  • date - formats timestamps with patterns like 'short', 'fullDate', or 'dd/MM/yyyy'
  • currency - formats numbers as money with locale and symbol support
  • number (DecimalPipe) - controls decimal precision: {{ 3.14159 | number:'1.2-3' }} gives 3.142
  • uppercase, lowercase, titlecase - text case conversion
  • slice - cuts arrays or strings: {{ 'Angular' | slice:0:3 }} gives Ang
  • json - serializes objects for debugging: <pre>{{ user | json }}</pre>
  • async - subscribes to an Observable or Promise and handles cleanup automatically

Custom pipes

To create a custom pipe, implement PipeTransform and decorate the class with @Pipe:

typescript
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'truncate', standalone: true }) export class TruncatePipe implements PipeTransform { transform(value: string, limit = 50, trail = '...'): string { if (!value) return ''; if (value.length <= limit) return value; return value.substring(0, limit) + trail; } }
html
<p>{{ longText | truncate }}</p> <!-- 50 chars... --> <p>{{ longText | truncate:20 }}</p> <!-- 20 chars... --> <p>{{ longText | truncate:30:'→' }}</p> <!-- 30 chars→ -->

The name in @Pipe is what you write in the template after |. Extra colon-separated values in the template map to additional arguments in transform(value, arg1, arg2, ...).

Pure vs impure

By default, every pipe is pure. Angular only reruns it when the input reference changes. So {{ items | sort }} will not rerun if you push an item into the same array, because the reference did not change.

Pure (default)Impure (pure: false)
Reruns whenInput reference changesEvery change detection cycle
PerformanceEfficientCan be slow
Use caseStateless formattingOutput depends on external state

Impure pipes are necessary when the output depends on something outside the input, like the current time. But they run constantly. On a list of 500 items, that is 500 transform() calls per cycle. Use them sparingly, and consider an interval-driven RxJS service instead for live-updating values.

The AsyncPipe

async subscribes to an Observable or Promise and automatically unsubscribes when the component is destroyed. Without it, you manage subscriptions manually, which is easy to forget and causes memory leaks. In practice, this is the pipe that comes up most in real Angular codebases.

typescript
@Component({ template: ` <div *ngIf='user$ | async as user'> {{ user.name }} </div> ` }) export class UserComponent { user$ = this.userService.getUser(1); constructor(private userService: UserService) {} }

No subscribe(), no unsubscribe(), no ngOnDestroy. The pipe handles all of it.

Common mistakes

Mistake 1: marking a stateless pipe as impure

typescript
// Wrong - runs on every change detection cycle @Pipe({ name: 'formatLabel', pure: false }) // Right - stateless formatting does not need impure @Pipe({ name: 'formatLabel' }) // pure: true is the default

On a list of 1,000 items, a formatting pipe marked pure: false calls transform() 1,000 times per cycle. That is not a formatting problem, that is a performance problem.

Mistake 2: mutating the input inside the pipe

typescript
// Wrong - sorts the original array in place transform(items: any[]): any[] { return items.sort((a, b) => a.name.localeCompare(b.name)); } // Right - return a new array transform(items: any[]): any[] { return [...items].sort((a, b) => a.name.localeCompare(b.name)); }

Pure pipes assume inputs do not change. Sorting the original array modifies the component data and breaks Angular's change detection assumptions.

Mistake 3: forgetting to register the custom pipe

typescript
// Standalone component - import the pipe @Component({ imports: [TruncatePipe], template: `<p>{{ text | truncate }}</p>` }) // NgModule-based - declare it @NgModule({ declarations: [TruncatePipe] })

If the pipe is not registered, Angular throws at compile time: The pipe 'truncate' could not be found.

Mistake 4: wrong order when chaining pipes

typescript
// Wrong - currency returns a string, number expects a number {{ price | currency | number:'1.2-2' }} // Right - apply numeric formatting before currency {{ price | number:'1.2-2' | currency:'USD' }}

Each pipe's output becomes the next pipe's input. Check what type each pipe returns before chaining.

Real-world usage

  • Date columns in data tables: {{ row.createdAt | date:'mediumDate' }}
  • Price display in product listings: {{ item.price | currency:'EUR' }}
  • Observable data without manual subscriptions: {{ users$ | async }}
  • Truncating long descriptions in UI cards (custom TruncatePipe)
  • Consistent phone number display across multiple templates (custom PhoneFormatPipe)
  • Debugging component state during development: <pre>{{ data | json }}</pre>

Decision rule: if the same formatting appears in 2 or more templates, create a pipe. If it is one-off formatting, a component method works fine.

Follow-up questions

Q: What is the difference between a pure and impure pipe, and when would you choose impure?
A: Pure pipes only rerun when the input reference changes. Impure pipes rerun on every change detection cycle. Choose impure only when the output depends on external state that changes independently, like the current time. For all stateless formatting, keep the pipe pure.

Q: Can you call a pipe in TypeScript code, not just in templates?
A: Yes. Instantiate the class directly: new DatePipe('en-US').transform(date, 'short'). This is uncommon though. Usually you transform data in the component or service and use the pipe only for template display.

Q: If I apply a custom pipe to 1,000 list items, does Angular run transform() 1,000 times?
A: On first render, yes. After that, a pure pipe caches results per input reference and only reruns for items whose reference changed. An impure pipe runs all 1,000 times on every cycle. That is why impure pipes and large lists are a bad combination.

Q: How do you pass multiple parameters to a pipe?
A: Separate them with colons: {{ value | date:'fullDate':'UTC' }}. The input value is the first argument to transform(), and each colon-separated value maps to the next argument in order.

Examples

Basic: built-in pipes for everyday formatting

html
<!-- Dates --> <p>{{ today | date:'short' }}</p> <!-- 1/15/26, 2:30 PM --> <p>{{ today | date:'fullDate' }}</p> <!-- Thursday, January 15, 2026 --> <p>{{ today | date:'dd/MM/yyyy' }}</p> <!-- 15/01/2026 --> <!-- Currency and numbers --> <p>{{ price | currency }}</p> <!-- $1,234.50 --> <p>{{ price | currency:'EUR' }}</p> <!-- €1,234.50 --> <p>{{ 3.14159 | number:'1.2-3' }}</p> <!-- 3.142 --> <!-- Text case --> <p>{{ 'hello world' | uppercase }}</p> <!-- HELLO WORLD --> <p>{{ 'hello world' | titlecase }}</p> <!-- Hello World --> <!-- Chained --> <p>{{ birthday | date:'fullDate' | uppercase }}</p> <!-- THURSDAY, JANUARY 15, 2026 -->

These cover the majority of display formatting in day-to-day Angular work. No extra component logic needed.

Intermediate: custom pipe for phone number formatting

typescript
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'phoneFormat', standalone: true }) export class PhoneFormatPipe implements PipeTransform { transform(value: string): string { if (!value || value.length < 10) return value; const cleaned = value.replace(/\D/g, ''); const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/); return match ? `(${match[1]}) ${match[2]}-${match[3]}` : value; } }
html
<p>{{ '5551234567' | phoneFormat }}</p> <!-- (555) 123-4567 --> <p>{{ '' | phoneFormat }}</p> <!-- empty string, no crash -->

The null check on line 5 is intentional. A pipe should always handle bad input gracefully because real data is unpredictable. Return the original value when formatting fails, never throw.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?