Angular – Pipes

June 27, 20263 min readUpdated 8/21/2026

A pipe formats a value for display. You call it in a template with |:

<td class="small">{{ row.createdAt | date: 'mediumDate' }}</td>

React has no equivalent and does not need one — a JSX template is JavaScript, so formatDate(row.createdAt) is already available. An Angular template is not JavaScript, so a formatting function has to be either a method on the component, repeated everywhere that formats a date, or a pipe: declared once, imported where needed.

The built-in ones worth knowing

DatePipe, CurrencyPipe, DecimalPipe, PercentPipe, UpperCasePipe, TitleCasePipe, JsonPipe (useful for debugging), SlicePipe, KeyValuePipe, and AsyncPipe.

Arguments come after a colon, and pipes chain left to right:

{{ order.createdAt | date: 'mediumDate' }}
{{ name | titlecase | slice: 0 : 20 }}

Writing one

A class with a decorator and one method:

@Pipe({ name: 'money' })
export class MoneyPipe implements PipeTransform {
  transform(value: number | null | undefined): string {
    return formatMoney(value ?? 0);
  }
}

The name is what the template writes after the bar. The first argument to transform is the piped value; any further arguments are what follows the colons.

Note the signature accepts null | undefined and defaults to zero. Both happen for real — an optional field on a DTO, or a total read before its request has landed — and without it the page prints $NaN.

Angular ships a CurrencyPipe that does very nearly this. MoneyPipe exists so that both frontends format money through the same formatMoney function and cannot drift apart on rounding. That is the usual reason to write a pipe that duplicates a built-in: not formatting, but a single source of truth.

The same file carries a second one for enum values from the API:

@Pipe({ name: 'humanise' })
export class HumanisePipe implements PipeTransform {
  transform(value: string | null | undefined): string {
    return value ? humanise(value) : '';
  }
}

PENDING_PAYMENT becomes Pending payment, in the orders table, the admin screens and the cart alike.

Using one

A pipe is imported by the component whose template uses it, exactly like a child component:

@Component({
  selector: 'app-product-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [MoneyPipe],

Pure and impure

This is the part that matters for performance, and the default is the one you want.

A pure pipe — the default — re-runs only when its input changes. Angular compares by reference, the way it does for OnPush inputs.

An impure pipe (@Pipe({ name: 'x', pure: false })) re-runs on every change-detection pass. It is the classic way to make a list slow: put an impure pipe in a loop over 200 rows and it runs 200 times per pass, for every pass, forever.

The usual reason people reach for impure is that a pure pipe does not notice a mutated array — items.push(x) does not change the reference, so the pipe does not re-run. The fix is not an impure pipe. The fix is to stop mutating: replace the array, and everything downstream notices.

AsyncPipe

| async subscribes to an observable, renders its latest value, and unsubscribes when the component is destroyed. For years it was how data reached an Angular template.

The pizza app does not use it once. Its state is signals, and a signal is read by calling it — no subscription to manage, so nothing for async to do. Where an observable does appear, toSignal converts it at the boundary rather than leaving it for the template.

You will still meet | async constantly in existing code, and it is still correct. It is simply not the default any more.

When not to use a pipe

For a value derived from component state, a computed is usually better: it is plain TypeScript, it is testable without a template, and it caches. A pipe earns its place when the formatting is shared across components — which is exactly the case for money and for enum names.

What is next

Styling: view encapsulation, :host, and how a Sass theme layers on top of Bootstrap without a single !important.