Angular Output Signals: output(), Event Payloads, and When to Still Use EventEmitter [2026]

Link copied
Angular Output Signals: output(), Event Payloads, and When to Still Use EventEmitter [2026]

Angular Output Signals: output(), Event Payloads, and When to Still Use EventEmitter [2026]

Components talk to parents through outputs. The output() function — added in v17.3 and the standard in v19+ — replaced the legacy @Output() EventEmitter<T>() pattern with a simpler signature: no EventEmitter reference, no .next() confusion with RxJS, no nullable type. This lesson walks the modern shape, the legacy holdovers, and the patterns you will actually use day to day.

This is lesson 2.5 of the Angular Tutorial, following lesson 2.4 on input signals. Inputs flow data parent → child; outputs flow events child → parent. With both lessons under your belt, you can design any parent-child component pair.

The output() shape #

A single function call creates an output:

import { Component, output } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="inc()">+1</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class CounterComponent {
  changed = output<number>();             // emits a number
  reset   = output();                      // emits void

  private count = 0;
  inc()   { this.count++; this.changed.emit(this.count); }
  reset() { this.count = 0; this.reset.emit(); }
}

The parent listens with event binding (lesson 2.2):

<app-counter (changed)="onCounterChanged($event)" (reset)="resetThings()" />

Three facts:

  1. output<T>() declares the payload type. output<number>() requires .emit(<number>); output() (no generic) requires .emit() with no argument.
  2. $event is the payload. In the parent template, $event is typed as the child's emitted type.
  3. The output is NOT a signal. It has only one method: .emit(). You cannot read its "current value" — it is a fire-and-forget pipe.

Output vs input — the symmetry #

A quick comparison table summarizes everything about both:

Feature input() output()
Data direction Parent → child Child → parent
Implementation Signal (read with ()) Emitter (call .emit(...))
Required generic Optional (inferred) Optional (defaults to void)
Required variant input.required<T>() None — outputs are always optional
Transform option Yes No
Alias option Yes Yes
Reactivity Tracked in computed/effect Not reactive (events)

Use inputs for state, outputs for events. The signature naming will be obvious in real components.

Aliases #

Outputs accept the same alias: option as inputs (lesson 2.4):

export class FormFieldComponent {
  valueChanged = output<string>({ alias: 'valueChange' });   // pair this with input named 'value' to enable [(value)]
}

The template uses the alias: (valueChange)="...". Aliases are most often used to enable the [(prop)] two-way binding sugar, which Angular recognizes by the naming convention propChange.

For a cleaner approach to two-way binding, use model() (lesson 3.3) — it packages an input + an output of the same type into a single primitive.

Emitting from anywhere #

The output can be emitted from anywhere in the component class — event handlers, lifecycle hooks, async callbacks:

export class SearchBoxComponent {
  searched = output<string>();
  private debounceTimer?: ReturnType<typeof setTimeout>;

  onInput(query: string) {
    clearTimeout(this.debounceTimer);
    this.debounceTimer = setTimeout(() => {
      this.searched.emit(query);
    }, 300);
  }
}

The parent receives only the debounced events. Outputs are how a component encapsulates internal logic and only surfaces the meaningful moments.

Output from an Observable #

If an external source (RxJS observable, a WebSocket, a fromEvent stream) is what triggers your emissions, the outputFromObservable() helper bridges them:

import { outputFromObservable } from '@angular/core/rxjs-interop';
import { fromEvent, map } from 'rxjs';

export class MouseTrackerComponent {
  private host = inject(ElementRef);
  pointerMoved = outputFromObservable(
    fromEvent<PointerEvent>(this.host.nativeElement, 'pointermove').pipe(
      map(e => ({ x: e.offsetX, y: e.offsetY })),
    ),
  );
}

This is the cleanest path when your emissions originate in a stream. Cleanup is automatic — the subscription dies with the component (via DestroyRef under the hood).

The legacy EventEmitter pattern #

The pre-v17 syntax still works in 2026 codebases:

import { EventEmitter, Output } from '@angular/core';

export class CounterComponent {
  @Output() changed = new EventEmitter<number>();
  @Output() reset = new EventEmitter<void>();

  inc()   { this.changed.emit(this.count); }
  reset() { this.reset.emit(); }
}

Differences from output():

Aspect Legacy @Output() + EventEmitter Modern output()
Boilerplate Decorator + new EventEmitter<T>() Single function call
Implementation Extends RxJS Subject Standalone emitter
RxJS confusion .next(), .error(), .complete() all callable Only .emit() exists
Async pipe in parent Possible (it's an observable) Not applicable
Cleanup Manual complete() in OnDestroy Automatic via DestroyRef

The new output() is functionally narrower and that is the point. It does the one thing emitters should do — fire events — without the broader RxJS surface.

Migration: the Angular CLI ships an automated transform: ng generate @angular/core:signal-input-migration --output (and a separate output-migration option). Run it on legacy codebases.

When to still use EventEmitter #

Three cases keep EventEmitter alive:

  1. Libraries you maintain and consumers are on older Angular. EventEmitter is supported by every version since v2; output() requires v17.3+.
  2. You need RxJS interop without outputFromObservable(). A consumer wants to .subscribe() to the output. output() has no .subscribe() — but a (name)="handler($event)" binding works the same way.
  3. Migrating slowly. A big codebase can leave existing outputs alone and only use output() for new components.

For new code in new projects: always output().

Type narrowing for output payloads #

The payload type flows to $event in the parent template:

// Child
selected = output<{ id: number; label: string }>();

// Parent handler — $event is typed automatically
onSelected(e: { id: number; label: string }) { /* ... */ }
<app-list (selected)="onSelected($event)" />

If your payload is a discriminated union, narrow inside the handler:

type Outcome =
  | { kind: 'saved'; id: number }
  | { kind: 'cancelled' }
  | { kind: 'error'; reason: string };

export class DialogComponent {
  done = output<Outcome>();
}

// Parent
onDone(o: Outcome) {
  switch (o.kind) {
    case 'saved':     handleSave(o.id); break;
    case 'cancelled': dismiss(); break;
    case 'error':     toast(o.reason); break;
  }
}

Discriminated unions in outputs are the cleanest pattern for multi-state dialogs.

Performance: outputs are NOT zone-aware #

When you call .emit(), Angular triggers a change-detection tick. In zoneless mode (default in v21+), this works through the signal graph; in zone-based mode it goes through the zone interceptor. Either way, a single emit triggers a CD pass for the affected components.

Do not emit in tight loops:

// Bad — 1000 CD passes
for (const item of items) this.itemTouched.emit(item);

// Good — one emit with the batch
this.batchTouched.emit(items);

Design your output API to emit at meaningful boundaries (drag-end, search-complete, save-finished), not on every micro-update.

Output forwarding patterns #

A common composition need: a wrapper component should forward a child's output unchanged.

@Component({
  selector: 'app-card',
  template: `
    <header (click)="clicked.emit($event)">{{ title() }}</header>
    <button (click)="removed.emit()">×</button>
  `,
})
export class CardComponent {
  title   = input.required<string>();
  clicked = output<MouseEvent>();
  removed = output();
}

// Parent of <app-card>
<app-card title="Hello" (clicked)="onClick($event)" (removed)="onRemove()" />

Forwarding is verbose because Angular is explicit. There is no (*)="forward($event)" syntax. If you find yourself forwarding many outputs, reconsider whether <ng-content> (lesson 2.6) would let the parent inject the inner markup directly instead.

Common gotchas #

Symptom Cause Fix
Property 'subscribe' does not exist on type 'OutputEmitterRef' You tried to subscribe() to an output() — that API only exists on EventEmitter Use a template event binding (name)="handler($event)" instead, or switch the field to outputFromObservable()
$event is typed as any in parent The child's output has no generic, so Angular infers void Add a generic: output<MyType>()
Output fires but parent doesn't react Parent's event binding misspelled ((saveed) instead of (saved)) strictTemplates catches this — turn it on
Output not received from a forwarded <ng-content> Outputs from projected components don't bubble through <ng-content> automatically Forward them explicitly, or use directive-based projection
Two-way binding [(prop)] doesn't compile The child has output<T>() but the matching alias propChange is missing Either match the naming convention (alias propChange), or use model<T>() instead

What's next #

Lesson 2.6 covers content projection — <ng-content> for letting the parent inject children into your component's template. Lesson 2.7 catalogs all lifecycle hooks. Together they round out the component-composition toolkit.

Lessons 2.8 through 2.13 (the second half of Module 2) handle pipes, directives, ng-template/ng-container, styling, host elements, and signal queries — every templating primitive Angular has.

Try it yourself #

A <color-pill> that emits when toggled — uses both an input signal and an output emitter:

import { Component, input, output, computed } from '@angular/core';

@Component({
  selector: 'color-pill',
  template: `
    <button [style.background]="bg()" (click)="toggle()">
      {{ label() }} {{ on() ? '✓' : '' }}
    </button>
  `,
})
export class ColorPillComponent {
  label = input.required<string>();
  hue   = input('teal', { alias: 'color' });
  on    = input(false);
  toggled = output<boolean>();

  bg = computed(() => this.on() ? this.hue() : 'transparent');

  toggle() { this.toggled.emit(!this.on()); }
}

The parent owns the on state and updates it when the output fires — classic unidirectional data flow. With model() (lesson 3.3) the same component is even shorter.

YouI want to react to an output in TypeScript code, not in a template. Can I subscribe to it?
Claude · used find_examplesYes — use the OutputEmitterRef.subscribe() method that output() exposes. It returns an OutputRefSubscription you can unsubscribe() from. But the cleaner pattern is to use a @ViewChild / viewChild() reference to the child and subscribe in the parent’s afterNextRender(). Alternatively, design your child to expose state via an output as a signal (using toSignal on its events observable) so the parent can compute on it. For most cases, the template event binding is enough.

Lesson 2.6 picks up content projection — letting parents inject markup into your component.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied When Angular is launched ? When Angular Landed: A History of the Popular Web Framework # Angular, …

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *