Lesson 12 of 30
A pipe is a pure function you can call from a template with `|` syntax — formatting for display, without cluttering the component class with presentation logic.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A pipe takes a value and transforms it for display, right inside the template: {{ price | currency }}, {{ createdAt | date:'mediumDate' }}. You could do the same formatting in the component class and expose a getter, but a pipe keeps that logic declarative, reusable across every template that needs it, and testable in isolation as what it really is — a function.
Angular ships several built-in pipes for the formatting you do constantly: date, currency, percent, decimalnumber, uppercase/lowercase, json (handy for debugging — dumps a value's JSON representation straight into the template), and async, which is different in kind from the rest — it doesn't format a value, it unwraps one. Bind async to an Observable or a Promise and it subscribes for you, hands the template the latest emitted value, and unsubscribes automatically when the component is destroyed, so you never hand-write a subscribe()/unsubscribe() pair just to get a value into a template.
You can also write your own. A custom pipe is a class with @Pipe({ name: '...' }) and a transform() method — the first argument is the value on the left of the |, any pipe arguments after : become additional parameters.
truncate pipeimport { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true,
// pure: true is the default — you almost never need to write this line
})
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength = 80): string {
if (value.length <= maxLength) return value;
return `${value.slice(0, maxLength).trimEnd()}…`;
}
}
<p>{{ comment.body | truncate:120 }}</p>
<p>Posted {{ comment.createdAt | date:'short' }}</p>
<p>{{ product.priceCents / 100 | currency:'USD' }}</p>
@if (latestOrder$ | async; as order) {
<p>Last order: {{ order.id }}</p>
}
TruncatePipe is pure by default — Angular only re-runs transform() when the input reference actually changes, not on every change-detection cycle. That's why pure: true is barely ever written explicitly: it's what you get for free, and it's the right default for almost every pipe, because most pipes are simple, cheap, deterministic transforms of their input. An impure pipe (pure: false) re-runs on every change-detection cycle regardless of whether its input changed — useful only for the rare case where a pipe needs to react to something outside its declared arguments (filtering a mutable array in place, for instance), and expensive enough that you should reach for it only when a pure pipe genuinely can't do the job.
async. A signal is already synchronous and already the current value — you just call it, {{ mySignal() }}, in the template. Piping a signal through async is a category error left over from Observable habits; async exists specifically to bridge Observables and Promises into the template, and a signal has no such gap to bridge.items.push(x)) instead of replacing it with a new array, a pure pipe watching items won't notice — this is the same reference-identity trap that trips people up with OnPush components, and the fix is the same: create a new array/object instead of mutating.pure: false is usually a sign the transform belongs in the component, not the template. An impure pipe re-runs on every change-detection cycle across the whole app wherever it's used, which is a real, easy-to-miss performance cost; almost every case that looks like it needs impurity can be redesigned as a pure pipe with a signal or computed value as its properly-changing input instead.A pure pipe's transform() is exactly the kind of function memoization exists for — same input, same output, safe to skip recomputing when nothing changed — which is precisely why Angular's default pipe behavior only re-runs on reference change instead of every cycle. If that reference-equality shortcut sounds familiar, it's the same idea the memoization lesson covers, just applied by the framework instead of by hand.