Lesson 20 of 30
toSignal() and toObservable() are the bridge between Angular's two reactivity systems — a stream on one side, a readable value on the other.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Angular now runs on two reactive systems at once: signals (signal(), computed()) for synchronous, glitch-free local state, and RxJS Observables for everything that's inherently a stream over time — HTTP responses, websocket messages, router events. Most real apps need both, and they need to talk to each other.
@angular/core/rxjs-interop gives you exactly two functions for that: toSignal() converts an Observable into a signal you can read with (), and toObservable() converts a signal into an Observable you can .pipe() and compose with operators. Neither replaces the other system — they're a bridge, not a migration path.
Here's the same debounced search from the RxJS fundamentals lesson, rebuilt so the search term itself lives in a signal instead of a Subject, and the results come back out as a signal too:
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
interface Result { id: string; title: string; }
@Component({
selector: 'app-search-box',
standalone: true,
template: `
<input (input)="query.set($any($event.target).value)" />
@for (r of results(); track r.id) {
<div>{{ r.title }}</div>
}
`,
})
export class SearchBoxComponent {
private http = inject(HttpClient);
query = signal(''); // Signal — the source of truth
private query$ = toObservable(this.query); // Signal -> Observable, for RxJS operators
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((term) => this.http.get<Result[]>(`/api/search?q=${term}`)),
),
{ initialValue: [] as Result[] }, // required: the HTTP call doesn't emit synchronously
);
}
An Observable is a push-based stream — it emits values over time, whenever something happens, not just once.
toObservable(this.query) exists purely so debounceTime/switchMap — RxJS operators — have an Observable to operate on; signals don't have .pipe(). Once that pipeline produces a new Observable, toSignal() converts it back into something the template reads with results(), no async pipe needed.
The initialValue option on toSignal() isn't optional decoration — a signal must always have a current value the moment it's created, but the piped Observable here won't emit anything until the first HTTP response comes back, milliseconds or seconds later. initialValue: [] is what the signal reads as in that gap. Skip it and TypeScript will complain that results might be undefined, since without it toSignal() has no synchronous value to hand back.
If you know your source Observable does emit synchronously on subscribe — say, a signal-backed BehaviorSubject, or another toObservable() output — you can pass { requireSync: true } instead of an initialValue. It skips the "might be undefined" case entirely, but throws at runtime if the assumption turns out wrong and nothing emits synchronously.
toSignal() without initialValue or requireSync makes the signal's type include undefined. That's not a limitation to work around — it's an honest reflection that the Observable genuinely hasn't produced a value yet at that point in time.toObservable() doesn't replay the signal's history. Subscribers only get values from the moment they subscribe onward, exactly like any other Observable — it's not secretly a BehaviorSubject with a memory of the past.toSignal/toObservable on every value in a component just to unify everything under signals usually adds indirection for no benefit — use them at the actual boundary where a stream-based API meets signal-based state, not everywhere.This lesson only exists because the previous one does — toObservable/toSignal are pure plumbing between the Observable model just covered and the signal model from the signals fundamentals lesson. It's a narrower version of the same problem TypeScript's structural typing lesson deals with in a different domain: two systems that model similar things differently, and a deliberate, well-defined seam for converting between them instead of silently coercing one into the other.