Operadores pipe, map, filter, tap

Intermedio
Angular
Angular
Actualizado: 24/09/2025

Transformación y filtrado

Los operadores de RxJS nos permiten procesar y modificar los datos que fluyen a través de los observables de manera declarativa y eficiente. En esta sección exploraremos los tres operadores fundamentales que todo desarrollador Angular debe conocer para trabajar con streams de datos.

Concepto de transformación y filtrado

La transformación consiste en modificar cada valor que emite un observable, convirtiéndolo en algo diferente según nuestras necesidades. Por ejemplo, podemos transformar un objeto de respuesta HTTP para extraer solo los datos que necesitamos, o convertir strings en números.

El filtrado, por otro lado, nos permite decidir qué valores del stream queremos que continúen y cuáles queremos descartar. Es como aplicar una condición que determina si un valor debe pasar o ser ignorado.

Operador map: transformando datos

El operador map es fundamental para transformar cada valor emitido por un observable. Funciona de manera similar al método map() de los arrays, aplicando una función de transformación a cada elemento.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

interface Usuario {
  id: number;
  name: string;
  email: string;
  address: {
    city: string;
    zipcode: string;
  };
}

@Component({
  selector: 'app-usuarios',
  template: `
    <div>
      @for (usuario of usuarios; track usuario.id) {
        <div class="usuario">
          <h3>{{ usuario.nombre }}</h3>
          <p>{{ usuario.ubicacion }}</p>
        </div>
      }
    </div>
  `
})
export class UsuariosComponent implements OnInit {
  usuarios: any[] = [];

  constructor(private http: HttpClient) {}

  ngOnInit() {
    // Transformamos la respuesta de la API para obtener solo los datos que necesitamos
    this.http.get<Usuario[]>('https://jsonplaceholder.typicode.com/users')
      .pipe(
        map(usuarios => usuarios.map(user => ({
          id: user.id,
          nombre: user.name.toUpperCase(), // Transformamos a mayúsculas
          ubicacion: `${user.address.city} (${user.address.zipcode})` // Combinamos datos
        })))
      )
      .subscribe(data => {
        this.usuarios = data;
      });
  }
}

En este ejemplo, map transforma cada usuario del array, creando nuevos objetos con propiedades renombradas y valores modificados.

Operador filter: seleccionando datos

El operador filter nos permite incluir solo aquellos valores que cumplan una condición específica. Los valores que no cumplan la condición simplemente no se emitirán.

import { Component, OnInit } from '@angular/core';
import { fromEvent } from 'rxjs';
import { map, filter } from 'rxjs/operators';

@Component({
  selector: 'app-buscador',
  template: `
    <input #searchInput type="text" placeholder="Buscar productos...">
    <div class="resultados">
      @for (resultado of resultados; track resultado.id) {
        <div class="producto">{{ resultado.nombre }}</div>
      }
    </div>
  `
})
export class BuscadorComponent implements OnInit {
  resultados: any[] = [];

  ngOnInit() {
    const searchInput = document.querySelector('input') as HTMLInputElement;
    
    // Creamos un observable desde los eventos de input
    fromEvent(searchInput, 'input')
      .pipe(
        map((event: any) => event.target.value), // Extraemos el valor
        filter(texto => texto.length >= 3), // Solo buscamos con 3+ caracteres
        map(texto => this.buscarProductos(texto)) // Transformamos a resultados
      )
      .subscribe(productos => {
        this.resultados = productos;
      });
  }

  private buscarProductos(termino: string) {
    const productos = [
      { id: 1, nombre: 'Laptop Gaming' },
      { id: 2, nombre: 'Mouse Inalámbrico' },
      { id: 3, nombre: 'Teclado Mecánico' }
    ];
    
    return productos.filter(p => 
      p.nombre.toLowerCase().includes(termino.toLowerCase())
    );
  }
}

El operador filter aquí evita realizar búsquedas con menos de 3 caracteres, mejorando la performance y experiencia del usuario.

Operador tap: efectos secundarios

El operador tap es especial porque no modifica los datos que fluyen por el stream. Su propósito es realizar efectos secundarios como logging, debugging o actualizar variables sin alterar el flujo de datos.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map, filter, tap } from 'rxjs/operators';

@Component({
  selector: 'app-productos',
  template: `
    <div class="loading" *ngIf="cargando">Cargando productos...</div>
    
    @for (producto of productos; track producto.id) {
      <div class="producto">
        <h3>{{ producto.nombre }}</h3>
        <p>Precio: {{ producto.precio | currency }}</p>
      </div>
    }
  `
})
export class ProductosComponent implements OnInit {
  productos: any[] = [];
  cargando = false;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.cargando = true;
    
    this.http.get<any[]>('https://api.ejemplo.com/productos')
      .pipe(
        tap(data => console.log('Datos recibidos:', data)), // Debugging
        tap(() => console.log('Iniciando filtrado...')), // Log de proceso
        filter(productos => productos.length > 0), // Solo si hay productos
        map(productos => productos.map(p => ({
          id: p.id,
          nombre: p.title,
          precio: p.price
        }))),
        tap(productos => console.log(`Productos procesados: ${productos.length}`)), // Log final
        tap(() => this.cargando = false) // Actualizar estado de carga
      )
      .subscribe(data => {
        this.productos = data;
        console.log('Productos listos para mostrar');
      });
  }
}

Inmutabilidad y funciones puras

Es importante entender que todos estos operadores son funciones puras. Esto significa que no modifican el observable original ni los datos que reciben. En su lugar, crean nuevos observables con las transformaciones aplicadas.

// El observable original permanece intacto
const numeros$ = of(1, 2, 3, 4, 5);

// Cada operador crea un nuevo observable
const numerosPares$ = numeros$.pipe(
  tap(n => console.log('Número original:', n)), // No modifica el stream
  filter(n => n % 2 === 0), // Crea nuevo observable con números pares
  map(n => n * 10) // Crea nuevo observable multiplicando por 10
);

// numeros$ sigue emitiendo 1, 2, 3, 4, 5
// numerosPares$ emite 20, 40 (2*10, 4*10)

Combinando operadores

La verdadera utilidad de estos operadores surge cuando los combinamos para crear pipelines de procesamiento de datos complejos pero legibles:

this.http.get<any[]>('https://api.tienda.com/productos')
  .pipe(
    tap(productos => console.log(`Recibidos ${productos.length} productos`)),
    filter(productos => productos.length > 0),
    map(productos => productos.filter(p => p.categoria === 'tecnologia')),
    tap(productos => console.log(`Filtrados ${productos.length} productos de tecnología`)),
    map(productos => productos.map(p => ({
      ...p,
      descuento: p.precio > 100 ? 0.1 : 0
    })))
  )
  .subscribe(productos => {
    this.productos = productos;
  });

Esta combinación nos permite crear flujos de datos declarativos y legibles, donde cada paso del procesamiento está claramente definido y es fácil de entender y mantener.

Operadores pipeable

El método pipe() es el mecanismo central que nos permite encadenar múltiples operadores RxJS de forma limpia y eficiente. Este enfoque modular transforma la manera en que procesamos streams de datos, proporcionando una sintaxis declarativa y fácil de mantener.

El método pipe() y su importancia

En las versiones modernas de RxJS, todos los operadores son pipeables, lo que significa que se aplican utilizando el método pipe() del observable. Esta aproximación reemplazó el patrón anterior de encadenamiento directo y ofrece múltiples ventajas.

import { of } from 'rxjs';
import { map, filter, tap } from 'rxjs/operators';

// Enfoque moderno con pipe()
const observable$ = of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

observable$
  .pipe(
    tap(valor => console.log('Valor recibido:', valor)),
    filter(n => n % 2 === 0),
    map(n => n * 2),
    tap(valor => console.log('Valor procesado:', valor))
  )
  .subscribe(resultado => console.log('Resultado final:', resultado));

Ventajas de los operadores pipeables

Los operadores pipeables ofrecen beneficios significativos sobre enfoques anteriores:

  • Tree-shaking mejorado: Solo importamos los operadores que realmente utilizamos
  • Composición funcional: Cada operador es una función pura que no modifica el observable original
  • Legibilidad: El flujo de transformación es claro y secuencial
  • Reutilización: Podemos crear funciones que encapsulen combinaciones de operadores
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter, tap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

@Component({
  selector: 'app-dashboard',
  template: `
    @if (estadisticas) {
      <div class="dashboard">
        <div class="metric">
          <h3>Usuarios activos</h3>
          <span>{{ estadisticas.usuariosActivos }}</span>
        </div>
        <div class="metric">
          <h3>Ventas del día</h3>
          <span>{{ estadisticas.ventasHoy | currency }}</span>
        </div>
      </div>
    }
  `
})
export class DashboardComponent implements OnInit {
  estadisticas: any = null;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.cargarDatos()
      .pipe(
        tap(() => console.log('Iniciando carga de datos del dashboard')),
        filter(response => response && response.success),
        map(response => response.data),
        tap(datos => console.log('Datos recibidos:', datos)),
        map(datos => this.procesarEstadisticas(datos)),
        catchError(error => {
          console.error('Error cargando dashboard:', error);
          return of(null);
        })
      )
      .subscribe(estadisticas => {
        this.estadisticas = estadisticas;
      });
  }

  private cargarDatos(): Observable<any> {
    return this.http.get<any>('/api/dashboard');
  }

  private procesarEstadisticas(datos: any) {
    return {
      usuariosActivos: datos.usuarios?.activos || 0,
      ventasHoy: datos.ventas?.hoy || 0
    };
  }
}

Encadenamiento y flujo de datos

Cuando utilizamos pipe(), cada operador recibe el resultado del operador anterior y pasa su resultado al siguiente. Este flujo secuencial hace que el código sea predecible y fácil de debuggear.

import { fromEvent } from 'rxjs';
import { map, filter, tap, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-buscador-avanzado',
  template: `
    <input #searchInput 
           type="text" 
           placeholder="Buscar productos..." 
           class="search-input">
    
    <div class="search-results">
      @for (producto of productos; track producto.id) {
        <div class="producto-card">
          <h4>{{ producto.nombre }}</h4>
          <p>{{ producto.descripcion }}</p>
          <span class="precio">{{ producto.precio | currency }}</span>
        </div>
      }
    </div>
  `
})
export class BuscadorAvanzadoComponent implements OnInit {
  productos: any[] = [];

  ngOnInit() {
    const searchInput = document.querySelector('.search-input') as HTMLInputElement;
    
    fromEvent(searchInput, 'input')
      .pipe(
        // Paso 1: Extraer el valor del input
        map((event: Event) => (event.target as HTMLInputElement).value),
        
        // Paso 2: Esperar 300ms sin nuevos cambios
        debounceTime(300),
        
        // Paso 3: Solo continuar si el valor cambió realmente
        distinctUntilChanged(),
        
        // Paso 4: Debug del valor que llega
        tap(termino => console.log('Término de búsqueda:', termino)),
        
        // Paso 5: Filtrar términos muy cortos
        filter(termino => termino.length >= 2),
        
        // Paso 6: Transformar a llamada de búsqueda
        map(termino => this.buscarProductos(termino)),
        
        // Paso 7: Log del resultado de la búsqueda
        tap(resultados => console.log(`Encontrados ${resultados.length} productos`))
      )
      .subscribe(productos => {
        this.productos = productos;
      });
  }

  private buscarProductos(termino: string) {
    // Simulación de base de datos de productos
    const todosLosProductos = [
      { id: 1, nombre: 'Smartphone Pro', descripcion: 'Teléfono de última generación', precio: 799 },
      { id: 2, nombre: 'Laptop Gaming', descripcion: 'Ordenador para juegos', precio: 1299 },
      { id: 3, nombre: 'Auriculares Bluetooth', descripcion: 'Audio inalámbrico premium', precio: 199 }
    ];

    return todosLosProductos.filter(producto => 
      producto.nombre.toLowerCase().includes(termino.toLowerCase()) ||
      producto.descripcion.toLowerCase().includes(termino.toLowerCase())
    );
  }
}

Creación de operadores personalizados

Una de las ventajas más importantes de los operadores pipeables es que podemos crear operadores personalizados reutilizables:

import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

// Operador personalizado para logging
function logStep<T>(paso: string) {
  return tap<T>(valor => console.log(`[${paso}]`, valor));
}

// Operador personalizado para formateo de precios
function formatearPrecio() {
  return map((producto: any) => ({
    ...producto,
    precioFormateado: `€${producto.precio.toFixed(2)}`
  }));
}

// Uso en el componente
@Component({
  selector: 'app-tienda',
  template: `
    @for (producto of productos; track producto.id) {
      <div class="producto">
        <h3>{{ producto.nombre }}</h3>
        <p>{{ producto.precioFormateado }}</p>
      </div>
    }
  `
})
export class TiendaComponent implements OnInit {
  productos: any[] = [];

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.http.get<any[]>('/api/productos')
      .pipe(
        logStep('Datos recibidos de la API'),
        map(productos => productos.filter(p => p.disponible)),
        logStep('Productos disponibles filtrados'),
        map(productos => productos.map(formatearPrecio())),
        logStep('Precios formateados')
      )
      .subscribe(productos => {
        this.productos = productos;
      });
  }
}

Buenas prácticas con pipe()

Para aprovechar al máximo los operadores pipeables, es importante seguir ciertas mejores prácticas:

  • Importación específica: Solo importa los operadores que necesitas
// Correcto: importación específica
import { map, filter, tap } from 'rxjs/operators';

// Evitar: importación general
import * as operators from 'rxjs/operators';
  • Orden lógico: Organiza los operadores en un orden que tenga sentido semánticamente
this.http.get<Usuario[]>('/api/usuarios')
  .pipe(
    tap(usuarios => console.log('Usuarios recibidos:', usuarios.length)), // Debug primero
    filter(usuarios => usuarios.length > 0), // Validación temprana
    map(usuarios => usuarios.filter(u => u.activo)), // Filtrado de datos
    map(usuarios => usuarios.map(this.transformarUsuario)), // Transformación final
    tap(usuarios => console.log('Usuarios procesados:', usuarios.length)) // Debug final
  )
  .subscribe(usuarios => this.usuarios = usuarios);
  • Legibilidad: Usa saltos de línea para mejorar la legibilidad de pipelines largos
// Fácil de leer y mantener
this.eventos$
  .pipe(
    debounceTime(300),
    distinctUntilChanged(),
    filter(evento => evento.tipo === 'click'),
    map(evento => evento.datos),
    tap(datos => this.actualizarAnalytics(datos)),
    catchError(this.manejarError.bind(this))
  )
  .subscribe(this.procesarEvento.bind(this));

Los operadores pipeables representan la evolución natural de RxJS hacia un enfoque más funcional y mantenible, permitiendo crear flujos de datos complejos de manera declarativa y eficiente.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Angular

Documentación oficial de Angular
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender el concepto de transformación y filtrado en observables.
  • Aprender a usar el operador map para transformar datos emitidos por un observable.
  • Aplicar el operador filter para filtrar valores según condiciones específicas.
  • Utilizar el operador tap para realizar efectos secundarios sin modificar el flujo de datos.
  • Entender el método pipe() para encadenar operadores de forma declarativa y crear pipelines legibles y eficientes.