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:
output<T>()declares the payload type.output<number>()requires.emit(<number>);output()(no generic) requires.emit()with no argument.$eventis the payload. In the parent template,$eventis typed as the child's emitted type.- 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:
- Libraries you maintain and consumers are on older Angular.
EventEmitteris supported by every version since v2;output()requires v17.3+. - 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. - 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.
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
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


