Back to CourseBeginner5 min read
Angular /

Built-in Pipes and Formatting

Format dates, numbers, currency and text in templates with Angular's built in pipes, and know which ones are expensive.

Bishal Biswas

Bishal Biswas

Self Learner

Published on: June 12, 2026
Last updated: September 7, 2026
Beginner Topic

A pipe transforms a display value

<p>{{ price | currency:'GBP' }}</p>
<p>{{ created | date:'medium' }}</p>
<p>{{ name | uppercase }}</p>

The | passes the value on the left into the pipe on the right. Arguments follow with colons.

Pipes are for display formatting. They do not change the underlying data.

Importing them

Each pipe is a separate standalone class.

import { DatePipe, CurrencyPipe, DecimalPipe } from '@angular/common';

@Component({
  imports: [DatePipe, CurrencyPipe, DecimalPipe],
  ...
})

Older code imports CommonModule instead, which brings all of them plus the legacy directives. Importing individually is leaner.

Text pipes

{{ 'hello' | uppercase }}          <!-- HELLO -->
{{ 'HELLO' | lowercase }}          <!-- hello -->
{{ 'hello world' | titlecase }}    <!-- Hello World -->
{{ longText | slice:0:100 }}       <!-- first 100 characters -->
{{ items | slice:1:4 }}            <!-- works on arrays too -->

slice takes the same arguments as Array.slice, including negatives:

{{ items | slice:-3 }}             <!-- last three -->

Number pipes

{{ 3.14159 | number }}             <!-- 3.142 -->
{{ 3.14159 | number:'1.2-2' }}     <!-- 3.14 -->
{{ 42 | number:'3.0-0' }}          <!-- 042 -->
{{ 0.256 | percent }}              <!-- 26% -->
{{ 0.256 | percent:'1.1-1' }}      <!-- 25.6% -->

The format string is minIntegerDigits.minFractionDigits-maxFractionDigits. So '1.2-2' means at least one digit before the point and exactly two after.

Currency

{{ 42.5 | currency }}                    <!-- $42.50, the default is USD -->
{{ 42.5 | currency:'GBP' }}              <!-- £42.50 -->
{{ 42.5 | currency:'INR' }}              <!-- ₹42.50 -->
{{ 42.5 | currency:'EUR':'code' }}       <!-- EUR42.50 -->
{{ 42.5 | currency:'GBP':'symbol':'1.0-0' }}   <!-- £43 -->

Arguments in order: currency code, display style, digit format, locale.

Set the application default rather than repeating the code everywhere:

providers: [
  { provide: DEFAULT_CURRENCY_CODE, useValue: 'GBP' },
  { provide: LOCALE_ID, useValue: 'en-GB' },
]

Date

{{ now | date }}                     <!-- Sep 7, 2026 -->
{{ now | date:'short' }}             <!-- 9/7/26, 2:15 PM -->
{{ now | date:'medium' }}            <!-- Sep 7, 2026, 2:15:30 PM -->
{{ now | date:'fullDate' }}          <!-- Monday, September 7, 2026 -->
{{ now | date:'dd/MM/yyyy' }}        <!-- 07/09/2026 -->
{{ now | date:'HH:mm' }}             <!-- 14:15 -->
{{ now | date:'yyyy-MM-dd' }}        <!-- 2026-09-07 -->
{{ now | date:'medium':'UTC' }}      <!-- in UTC -->
{{ now | date:'medium':'':'fr' }}    <!-- in French -->

It accepts a Date, a timestamp number, or an ISO string, which is what an API usually returns.

protected readonly created = '2026-09-07T14:15:30Z';   // works directly

Watch the format tokens. MM is month and mm is minutes, DD is not valid, and dd is day. Getting these confused is a classic bug.

json

<pre>{{ user | json }}</pre>

For debugging only. It stringifies with indentation, which beats seeing [object Object].

Do not ship it. It is slow on large objects and exposes everything on the object.

keyvalue

Iterating an object or a Map.

@for (entry of settings | keyvalue; track entry.key) {
  <p>{{ entry.key }}: {{ entry.value }}</p>
}

It sorts by key by default. Pass a comparison to change that, or null to keep insertion order:

@for (entry of settings | keyvalue:originalOrder; track entry.key) { ... }
protected originalOrder = () => 0;

async

The one that does real work. It subscribes to an observable or promise, returns the latest value, and unsubscribes when the component is destroyed.

import { AsyncPipe } from '@angular/common';

@Component({
  imports: [AsyncPipe],
  template: `
    @if (users$ | async; as users) {
      <p>{{ users.length }} users</p>
    }
  `,
})
export class UserList {
  protected readonly users$ = inject(HttpClient).get<User[]>('/api/users');
}

Before signals this was the standard way to render asynchronous data. It is still correct and still everywhere.

The important detail: each | async creates its own subscription. Using it twice on the same observable makes two HTTP requests.

<!-- two subscriptions, two requests -->
<p>{{ (users$ | async)?.length }}</p>
<p>{{ (users$ | async)?.[0]?.name }}</p>

<!-- one subscription -->
@if (users$ | async; as users) {
  <p>{{ users.length }}</p>
  <p>{{ users[0]?.name }}</p>
}

In new code, toSignal or httpResource is usually clearer:

protected readonly users = httpResource<User[]>(() => '/api/users');
@if (users.value(); as list) { <p>{{ list.length }} users</p> }

i18n pipes

{{ count | i18nPlural: pluralMap }}
{{ gender | i18nSelect: genderMap }}
protected readonly pluralMap = {
  '=0': 'No items',
  '=1': 'One item',
  other: '# items',
};

Useful for pluralisation without a full translation setup. # is replaced by the number.

Chaining

{{ created | date:'fullDate' | uppercase }}
{{ price | currency:'GBP' | slice:0:10 }}

Left to right. Readable up to two pipes, confusing beyond that. Move anything longer into a computed.

Performance

Pipes are pure by default, meaning they only recompute when the input reference changes. That makes them cheap.

{{ price | currency:'GBP' }}      <!-- recomputed only when price changes -->

The exceptions are async, json and keyvalue, which are impure and run on every change detection pass. Keep json out of production templates, and avoid keyvalue on large objects.

Locale

Date, number, currency and percent all depend on the locale, which defaults to en-US.

import { registerLocaleData } from '@angular/common';
import localeEnGb from '@angular/common/locales/en-GB';

registerLocaleData(localeEnGb);

providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }]

Without registering the locale data, non default locales throw at runtime.

Quick recap

  • A pipe formats a value for display, using | with colon separated arguments.
  • Import each pipe individually rather than pulling in CommonModule.
  • The number format string is minInt.minFrac-maxFrac.
  • In date formats, MM is month and mm is minutes.
  • async subscribes and unsubscribes for you, but each use is a separate subscription.
  • json and keyvalue are impure, so keep json out of production.
  • Set LOCALE_ID and DEFAULT_CURRENCY_CODE once instead of repeating arguments.
app.ts

import { Component, inject, signal, LOCALE_ID, DEFAULT_CURRENCY_CODE } from '@angular/core';
import { DatePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe,
         TitleCasePipe, JsonPipe, KeyValuePipe, AsyncPipe } from '@angular/common';
@Component({
  selector: 'app-pipes',
  // Import each pipe individually. CommonModule brings all of them plus more.
  imports: [DatePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe,
            TitleCasePipe, JsonPipe, KeyValuePipe, AsyncPipe],
  template: `
    <!-- text -->
    {{ 'hello world' | titlecase }}          <!-- Hello World -->
    {{ longText | slice:0:100 }}             <!-- also works on arrays -->
    {{ items | slice:-3 }}                   <!-- last three -->
    <!-- numbers: minInt.minFrac-maxFrac -->
    {{ 3.14159 | number:'1.2-2' }}           <!-- 3.14 -->
    {{ 42 | number:'3.0-0' }}                <!-- 042 -->
    {{ 0.256 | percent:'1.1-1' }}            <!-- 25.6% -->
    <!-- currency: code, display, digits -->
    {{ 42.5 | currency:'GBP' }}              <!-- £42.50 -->
    {{ 42.5 | currency:'GBP':'symbol':'1.0-0' }}   <!-- £43 -->
    <!-- dates. MM is MONTH, mm is MINUTES. Accepts a Date, number or ISO string. -->
    {{ created | date:'dd/MM/yyyy HH:mm' }}
    {{ created | date:'medium':'UTC' }}
    <!-- debugging only, and impure: it runs on EVERY check -->
    <pre>{{ user() | json }}</pre>
    <!-- iterate an object, sorted by key unless you pass a comparison -->
    @for (entry of settings | keyvalue; track entry.key) {
      <p>{{ entry.key }}: {{ entry.value }}</p>
    }
    <!-- async subscribes AND unsubscribes. Each use is its OWN subscription,
         so this pattern makes ONE request, not two. -->
    @if (users$ | async; as users) {
      <p>{{ users.length }}</p>
      <p>{{ users[0]?.name }}</p>
    }
  `,
})
export class Pipes {
  protected readonly created = '2026-09-07T14:15:30Z';   // an ISO string works
  protected readonly user = signal({ name: 'Asha' });
  protected readonly items = [1, 2, 3, 4, 5];
  protected readonly longText = 'a long piece of text';
  protected readonly settings = { theme: 'dark', locale: 'en-GB' };
  protected readonly users$ = inject(HttpClient).get<{ name: string }[]>('/api/users');
}
// Set these ONCE instead of repeating arguments in every template:
//   { provide: LOCALE_ID, useValue: 'en-GB' }
//   { provide: DEFAULT_CURRENCY_CODE, useValue: 'GBP' }
// Non default locales also need registerLocaleData(), or they throw at runtime.
// Pipes are PURE by default: recomputed only when the input reference changes.
// async, json and keyvalue are impure and run on every pass.

Was this explanation helpful?

Your feedback helps us improve the content for everyone.