Introduce signal introspection API: runtime metadata for signal types · Issue #68282 · angular/angular · GitHub
Skip to content

Introduce signal introspection API: runtime metadata for signal types #68282

@vs-borodin

Description

@vs-borodin

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

As the Angular signal ecosystem has evolved, the framework introduced several specialised signal variants beyond the base signal: component inputs (input, input.required), view queries (viewChild(ren), viewChild.required), and content queries (contentChild(ren), contentChild.required), as well as derived signals (computed, linkedSignal).

Image

Despite sharing a unified public interface (Signal<T>), each of these types carries distinct internal semantics that are critical for library authors to account for:

  • Lifecycle readiness: special signals such as input.required, viewChild.required, and contentChild.required cannot always be safely read synchronously before the first change detection cycle. Today, a library utility that accepts a Signal<T> has no way to detect this condition.
  • Change detection: query signals (viewChild, contentChild) are managed by Angular's change detection. Their value transitions are driven by the framework's render phases, not by user code. This distinction determines whether a library should use afterRenderEffect (delegating checks to Angular) or afterEveryRender (polling manually), a decision that currently requires introspecting private node properties such as _dirtyCounter.

In my library when implementing a utility of the form (pseudocode):

function onDisconnect<T extends Element>(target: Signal<T>, callback: (element: T) => void);

I need to inspect a signal to understand how I can more optimally track the disconnect logic. As mentioned above, at the moment I have to rely on accessing the node’s internal property via the [SIGNAL] symbol.

Proposed solution

A possible solution could be the following:

Introduce a reflectSignalType() function, following the precedent of reflectComponentType(), which returns a structured metadata object describing the runtime characteristics of a given signal:

interface SignalMetadata {
  kind:
    | 'signal'  // signal()
    | 'computed'  // computed()
    | 'linkedSignal'  // linkedSignal()
    | 'input'  // input() / input.required()
    | 'query'  // viewChild() / viewChild.required()
  required: boolean;
  writable: boolean;
  /* ... */
}

function reflectSignalType(signal: Signal<unknown>): SignalMetadata;

P.S.: In the internal implementation, there is already ReactiveNodeKind enum that describes all types of reactive nodes (including, for example, a pure consumer like effect). However, there is no separate kind for input/query signals. This might require further consideration.

Alternatives considered

Partial workarounds exist for introspection needs. It is possible to detect whether a signal is a query signal or an input signal by inspecting internal properties of the signal node accessed through the private [SIGNAL] symbol (an undocumented, unstable implementation detail with no guarantees across versions).

Library authors currently have no choice but to either ignore these distinctions entirely, or ask users to pass additional metadata manually alongside the signal - a significant ergonomic regression that puts the burden of framework-level knowledge onto the consumer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimegemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions