Skip to the solution
MediumFrontend9 min read

output() vs @Output() EventEmitter — the new signal outputs?

591 views

The problem

Question presented to candidate: "Angular 17.3 added output(). Our components use @Output() saved = new EventEmitter<Item>(). What is different about output(), is it actually a signal, and should we migrate?"

What a strong answer should cover:

  • output() returns an OutputEmitterRef<T> with emit() and subscribe(); the template side, (saved)="...", is unchanged.
  • It is not a signal (it holds no value) and not an RxJS Subject: no pipe, no next, no complete. Convert with outputToObservable() if you need operators.
  • Lifecycle: listeners are dropped when the component is destroyed; a late emit() only warns (NG0953) and a late subscribe() throws. A leftover EventEmitter subscription keeps firing.
  • Must be created in an injection context (field initializer), like input().
  • Aliases are an option: output<number>({ alias: "remove" }).
  • Stable since v19; @Output still supported; ng generate @angular/core:output-migration rewrites it.

Clarifying questions expected:

  • "Does any code call .pipe() or .subscribe() on these emitters directly, or are they only used from templates?"

Code / implementation expected: Yes — the same child with output() and with @Output() + EventEmitter.

  • outputs
  • signals
  • angular-17

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read · 12 sections

Read the code

AngularTodo item with output() and an aliased output (needs the Angular compiler)
import { Component, computed, input, output, signal } from "@angular/core";

type Todo = { id: number; text: string; done: boolean };

// CHILD: declares outputs with output(). No EventEmitter, no @Output, no RxJS.
@Component({
  selector: "todo-item",
  template: `
    <label>
      <input type="checkbox" [checked]="todo().done" (change)="toggled.emit(todo().id)" />
      {{ todo().text }}
    </label>
    <button (click)="removed.emit(todo().id)">x</button>
  `,
})
export class TodoItemComponent {
  todo = input.required<Todo>();
  toggled = output<number>();                    // OutputEmitterRef<number>
  removed = output<number>({ alias: "remove" }); // parent listens with (remove)
}

@Component({
  selector: "app-root",
  imports: [TodoItemComponent],
  template: `
    @for (t of todos(); track t.id) {
      <todo-item [todo]="t" (toggled)="toggle($event)" (remove)="remove($event)" />
    }
    <p>{{ left() }} left</p>
  `,
})
export class AppComponent {
  todos = signal<Todo[]>([
    { id: 1, text: "Write tests", done: false },
    { id: 2, text: "Ship it", done: false },
  ]);
  left = computed(() => this.todos().filter(t => !t.done).length);

  toggle(id: number) {
    console.log(`  parent got toggled(${id})`);
    this.todos.update(list => list.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
  }
  remove(id: number) {
    console.log(`  parent got remove(${id})`);
    this.todos.update(list => list.filter(t => t.id !== id));
  }
}
AngularThe same todo item with @Output() and EventEmitter
import { Component, EventEmitter, Input, Output, computed, signal } from "@angular/core";

type Todo = { id: number; text: string; done: boolean };

// The SAME child written with @Output() + EventEmitter (the pre-v17.3 way, still supported).
@Component({
  selector: "todo-item",
  template: `
    <label>
      <input type="checkbox" [checked]="todo.done" (change)="toggled.emit(todo.id)" />
      {{ todo.text }}
    </label>
    <button (click)="removed.emit(todo.id)">x</button>
  `,
})
export class TodoItemComponent {
  @Input({ required: true }) todo!: Todo;
  @Output() toggled = new EventEmitter<number>();        // an RxJS Subject under the hood
  @Output("remove") removed = new EventEmitter<number>(); // alias: parent listens with (remove)
}

@Component({
  selector: "app-root",
  imports: [TodoItemComponent],
  template: `
    @for (t of todos(); track t.id) {
      <todo-item [todo]="t" (toggled)="toggle($event)" (remove)="remove($event)" />
    }
    <p>{{ left() }} left</p>
  `,
})
export class AppComponent {
  todos = signal<Todo[]>([
    { id: 1, text: "Write tests", done: false },
    { id: 2, text: "Ship it", done: false },
  ]);
  left = computed(() => this.todos().filter(t => !t.done).length);

  toggle(id: number) {
    console.log(`  parent got toggled(${id})`);
    this.todos.update(list => list.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
  }
  remove(id: number) {
    console.log(`  parent got remove(${id})`);
    this.todos.update(list => list.filter(t => t.id !== id));
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 45 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

What is trackBy in ngFor and why use it?

Continue