skip to content

How do you write a custom Angular pipe with @Pipe and PipeTransform, give it an extra argument, and use it in a standalone component?

level: juniorimportance: must knowfreq 62%

answer

  1. a decorator plus one method
  2. first parameter is the piped value
  3. extra parameters become colon arguments
  4. import it where it is used

basics

~10 s

Decorate a class with @Pipe({ name: 'truncate' }), implement PipeTransform's transform(value, ...args), and add the class to the standalone component's imports. Extra transform parameters become colon-separated arguments, as in text | truncate: 40.

solid answer

~40 s

A custom pipe is a class with `@Pipe({ name: 'truncate' })` that implements `PipeTransform`. Its `transform(value, max = 20, suffix = '...')` receives the piped value first and each extra argument in order, written in the template as `{{ text | truncate: 40 : ' [more]' }}`. Since v19 pipes are standalone by default, so I add `TruncatePipe` to the component's `imports`. Pipes are pure by default, which is right when output depends only on the parameters. I handle `null`/`undefined`, keep the name camelCase with no hyphens, and move the logic into a plain function the pipe delegates to, so other code can reuse it without injecting the pipe.

code

ts · 21 lines
ts
import { Component, Pipe, PipeTransform, input } from '@angular/core';

export function truncate(value: string, max: number, suffix: string): string {
  return value.length > max ? value.slice(0, max) + suffix : value;
}

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
  transform(value: string | null | undefined, max = 20, suffix = '...'): string {
    return value ? truncate(value, max, suffix) : '';
  }
}

@Component({
  selector: 'app-note-card',
  imports: [TruncatePipe],
  template: `<p>{{ body() | truncate: 40 : ' [more]' }}</p>`,
})
export class NoteCard {
  body = input.required<string>();
}

go deeper

for a junior

Recall the @Pipe decorator with a name, the transform method, colon arguments and importing the pipe into a standalone component.

for a middle

Explain the defaults (pure, standalone since v19), strict typing of transform, null handling and how registration differs in NgModule code.

for a senior

Structure pipes so logic lives in plain functions, is unit-tested once and is reused by services without injecting pipe classes.

for a principal

Decide which transformations deserve shared pipes in a design system and which belong in data preparation, to keep templates thin.

## What a custom pipe is A **pipe** is a class Angular calls from a template expression through the `|` operator. Angular ships formatting pipes, but any project can add its own for transformations that repeat across templates: shortening text, formatting file sizes, mapping a status code to a label. A custom pipe has two required parts: - the **`@Pipe` decorator** with a `name`, the identifier used in templates; - a **`transform` method**, which receives the value on the left of `|` as its first parameter and returns what the template renders. The class should also implement the **`PipeTransform`** interface from `@angular/core`, which makes TypeScript check that `transform` exists with a compatible shape. ## A complete example ```ts import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'truncate' }) export class TruncatePipe implements PipeTransform { transform(value: string | null | undefined, max = 20, suffix = '...'): string { if (!value) return ''; return value.length > max ? value.slice(0, max) + suffix : value; } } ``` Using it from a standalone component: ```ts @Component({ selector: 'app-note-card', imports: [TruncatePipe], template: `<p>{{ note().body | truncate: 40 }}</p>`, }) export class NoteCard { note = input.required<{ body: string }>(); } ``` ## Arguments Every parameter after the first becomes a **pipe argument**. In the template each argument follows a colon, in declaration order: `{{ text | truncate: 40 : ' [more]' }}`. Default parameter values in `transform` make arguments optional, and the template type checker checks each argument against the parameter types. ## Metadata you should know | `@Pipe` option | Meaning | Default | |---|---|---| | `name` | The template name; camelCase, no hyphens | required | | `pure` | Re-run only when the value or an argument changes | `true` | | `standalone` | Usable through a component's `imports` without an NgModule | `true` since v19 | Naming follows the Angular guide's convention: the `name` in camelCase (`truncate`, `fileSize`) and the class in PascalCase with a `Pipe` suffix (`FileSizePipe`). ## Registering it 1. **Standalone components** (the default for new code): add the pipe class to the component's `imports` array. 2. **NgModule-based code**: add a standalone pipe to the module's `imports`, or, for an older non-standalone pipe, to the module's `declarations` and `exports`. A pipe used in a template but not imported fails at compile time with an error saying no pipe of that name was found. ## Good habits - **Keep `transform` pure in the everyday sense**: its output should depend only on its parameters. That is what makes the default `pure: true` correct. - **Handle `null` and `undefined`**: template values are often not loaded yet. - **Put the logic in a plain function** and let `transform` delegate to it. The Angular guide recommends this so services can reuse the logic without injecting the pipe class, which is not designed to be an injectable service. - **Type the signature precisely**, so misuse such as `{{ count | truncate }}` on a number is caught by strict template checking. ## Testing a simple pipe A pipe without dependencies is an ordinary class, so the fastest test needs no `TestBed` at all: ```ts const pipe = new TruncatePipe(); expect(pipe.transform('Angular pipes', 7)).toBe('Angular...'); expect(pipe.transform(null)).toBe(''); ``` Test the edge cases the template will hit: `null` while data loads, a value exactly at the limit, and the default arguments. If the logic lives in a plain function, test that function and keep one smoke test for the pipe. ## What a pipe is not for Pipes transform a value for display. They are not the place for side effects such as HTTP calls or writing to state, and they cannot be used in event bindings. When the result depends on data other than the arguments, the pipe must either receive that data as an argument or be marked impure, which is a separate discussion about purity.

  • What happens if a template uses {{ x | truncate }} but the component does not import TruncatePipe?
    The template compiler cannot resolve the name and reports that no pipe called truncate was found. Standalone components must list every pipe they use in `imports`; NgModule-based components get pipes from their module's imports or declarations.
  • Why does the Angular guide say not to inject a pipe class into a service?
    Pipes are template operators, not injectable services. Put the logic in a plain exported function, let the pipe's `transform` delegate to it, and import the function in the service. Built-in pipes have equivalents such as `formatDate` and `formatNumber`.

saying these in an interview costs you the question

  • Names a pipe with hyphens like 'my-truncate'
  • Thinks a pipe must be declared in an NgModule in current Angular
  • Believes pipes are impure unless marked pure: true
  • Passes pipe arguments as a parenthesised list
  • Injects the pipe class into services to reuse its logic