In Angular, what does `toSignal(obs$)` return before the first emission, and how do the `initialValue` and `requireSync` options change it?
answer
- undefined in the type and at runtime
- a value for the gap
- the source must emit during subscribe
- NG0601 when it does not
- null versus undefined
basics
~10 sAngular's toSignal() returns undefined until the first emission, typed Signal<T | undefined>; initialValue supplies a value for that gap, and requireSync: true removes it by requiring a synchronous emission, throwing NG0601 otherwise.
solid answer
~40 sWithout options, `toSignal(quotes$)` is typed `Signal<Quote | undefined>` and returns `undefined` until the Observable emits. `initialValue` fills that gap and sets the type: `{ initialValue: null }` gives `Signal<Quote | null>`, `{ initialValue: 0 }` on a number stream gives `Signal<number>`. `requireSync: true` says the source always emits **synchronously on subscribe** (a `BehaviorSubject`, a `startWith`, a store's current state); the type becomes `Signal<Quote>` with no initial value, and if the source does not emit during subscribe `toSignal` throws `NG0601` immediately. So use `requireSync` for sources that hold a current value, `initialValue` for sources that do not, and accept `undefined` only when the template genuinely handles a loading state.
code
ts · 21 linesimport { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { BehaviorSubject } from 'rxjs';
import { PriceFeed } from './price-feed';
@Component({
selector: 'app-watchlist-header',
template: `
@let q = quote();
@if (q === null) { <span>Connecting...</span> } @else { <span>{{ q.price }}</span> }
<small>{{ currency() }}</small>
`,
})
export class WatchlistHeader {
private readonly currency$ = new BehaviorSubject<'USD' | 'EUR'>('USD');
// WebSocket feed: no synchronous value, so give the gap an honest placeholder.
readonly quote = toSignal(inject(PriceFeed).quotes$, { initialValue: null });
// BehaviorSubject: emits on subscribe, so requireSync removes undefined from the type.
readonly currency = toSignal(this.currency$, { requireSync: true });
}go deeper
Remember that toSignal gives undefined until the first value, and that initialValue lets you choose a different starting value.
Explain how each option changes the signal's type, what counts as a synchronous emission, and when NG0601 is thrown.
Choose honest placeholders over fake values in business-critical screens, and use requireSync to make wrong assumptions about a source fail loudly.
Standardise loading-state conventions for signal-based components so teams do not mix undefined, null and fake defaults across a product.
## Signals always have a value; Observables may not yet A signal must return something the moment it is read. An Observable such as a WebSocket feed or an HTTP call may not have produced anything when `toSignal()` subscribes. The options decide what the signal returns in that gap, and they also decide the **TypeScript type** of the signal. ## The three configurations | Call | Before first emission | Signal type | |---|---|---| | `toSignal(quotes$)` | `undefined` | `Signal<Quote \| undefined>` | | `toSignal(quotes$, { initialValue: null })` | `null` | `Signal<Quote \| null>` | | `toSignal(count$, { initialValue: 0 })` | `0` | `Signal<number>` | | `toSignal(state$, { requireSync: true })` | never observed; must emit during subscribe | `Signal<State>` | ### Default: undefined With no options the internal state starts as `undefined`. Templates must handle it: `quote()?.price`, an `@if (quote(); as q)` block, or a `@let` with an explicit check. Angular's guide compares this with the async pipe, which returns `null` in the same situation. ### initialValue `initialValue` is the value the signal returns until the Observable emits for the first time. Its type is merged into the signal's type, so `null` adds `| null` and a value of the stream's own type removes the `undefined` entirely. A custom `equal` function, if given, is also applied against the initial value, so an emission equal to it does not notify consumers. Pick a value that means something: `null` for "not loaded yet", `0` for a counter, `[]` for a list. A fake price of `0` in a trading screen is a lie the user can act on; `null` plus a "Connecting..." placeholder is honest. ### requireSync `requireSync: true` asserts that the Observable **emits synchronously during the subscribe call**. Sources that do this include: - a `BehaviorSubject` (it replays its current value to each subscriber); - any stream ending in `startWith(...)`; - stores that replay their current state to each new subscriber. With `requireSync` the signal type has no `undefined`, and no initial value is allowed. If the source does not emit during subscribe, `toSignal()` throws **`NG0601`** right away with the message that `toSignal()` was called with `requireSync` but the Observable did not emit synchronously. An `HttpClient` call or a WebSocket stream therefore cannot use it. ## Choosing 1. Does the source **always hold a current value**? Use `requireSync: true`, and let a wrong assumption fail loudly with `NG0601`. 2. Is there a **meaningful placeholder**? Use `initialValue`. 3. Does the UI need a **distinct loading state**? Use `initialValue: null` (or accept `undefined`) and branch on it explicitly. ## Pitfalls - **Truthiness checks**: `@if (count(); as c)` hides a real `0`; compare against `null` or `undefined` instead. - **Non-null assertions**: `quote()!.price` compiles and throws at runtime during the gap. - **`requireSync` on a replay stream that is still empty**: a `ReplaySubject` with nothing buffered does not emit on subscribe, so it throws `NG0601`. ## What interviewers listen for - The default is **`undefined`**, reflected in the type. - **`initialValue`** fills the gap and narrows the type. - **`requireSync`** needs a synchronous emission and fails with **`NG0601`** otherwise. - A deliberate choice between a **placeholder** and a **loading state**.
- Why can't you pass both initialValue and requireSync: true?They answer the same question in opposite ways. `requireSync` promises the source provides the first value during subscribe, so an initial value would never be observed; the overloads only accept `requireSync: true` with `initialValue` left undefined.
- When is NG0601 thrown: at the first read or when toSignal is called?When `toSignal` is called. Right after subscribing it checks whether the state is still empty, and throws `NG0601` immediately if the source did not emit synchronously, so the mistake surfaces at construction rather than at some later read.
saying these in an interview costs you the question
- toSignal returns null before the first emission, like the async pipe
- requireSync makes toSignal wait for the first HTTP response
- initialValue is emitted into the Observable as its first value
- requireSync works with any Observable that eventually emits
- A ReplaySubject with an empty buffer satisfies requireSync