Interactive Example
What follows is a peek behind the curtain at what the Angular Team hasn't officially documented yet...
⚠️ a word of caution:
import { ɵinput as input } from '@angular/core';
don't try this in production!
The StackBlitz project below lets you explore Signal Input declarations firsthand. Right now, Angular (v17.1.0-next.5) doesn't include the actual Signal Input runtime, so we're limited to examining the type declarations and the public surface they expose.
Table of Contents
🛠️ first - code examples
🥩 then - the underlying types
We'll walk through each example from the StackBlitz above in detail, and after that, we'll dissect the internal type machinery that powers them.
Illustrative Scenarios
Let's kick things off with a series of examples. Here's the first one:
🔍 Similar to standard signals, the initial value determines the input's type:
⚠️ It's crucial to remember that Input Signals are READONLY. This is logical because it's the parent component that provides new values through the template, not the component itself 😉
A design choice I particularly admire:
💪 It leverages (1) the capabilities of #typescript and (2) adopts effective patterns from other frameworks, notably React.
💜🩷💜 Absolutely love it 🩷💜🩷
While less thrilling - and IMHO it's best minimized since it can hinder code readability and obscure component data flow - aliasing is supported:
📡 The parent component uses the aliased name to pass data
🔐 Internally, the property name is used (naturally)
Another well-thought-out decision:
💪 Since an input is REQUIRED, providing an initial value would be illogical 😉, so it's excluded from the function's signature.
In this scenario, the initialValue parameter is omitted from the function's overload signature. More accurately, it's never permitted to be present there 😉.
Consequently, TypeScript would attempt to pass your initial value as an options object, which leads to a type error.
One more illustration - the transform option is only permissible when both ReadT and WriteT type parameters are supplied. We'll delve deeper into this shortly...
Under the Hood
Time to examine 🍖 the internal workings 🥩
For a high-level view, I recommend checking out the ➡️ Angular Team's Sub-RFC 3: Signal-based Components, which covers the strategy for using signals in components. You can also ➡️ examine the source code directly. Anyway, let's get into the details!
We encounter two new signal-related symbols:
This follows the same pattern as the original unique Signal symbol, which was designed to limit compatibility between various (input) signals.
For instance:
✅ You can assign an InputSignal<number, number> to a standard Signal<number> variable.
Here's the demonstration:
We first need to make the standard signal READONLY so that TypeScript can compare two readonly signals (input signals are READONLY).
Where a string signal is needed, a string input signal is valid because it possesses all the necessary properties.
❌ The reverse fails, as a standard string signal lacks the branded read/write properties.
Reassigning signals is unlikely to be a common operation 😉. However, the Angular compiler leverages these symbols in its own processes.
Now, this is the source of the signals API. There are distinct implementations for optional and required inputs. The public API is simply a convenient wrapper:
s1 = input()
s2 = input.required()
Unfortunately, we aren't able to observe its runtime behavior at this moment.
(Important note: as stated, ng v17.1 is imminent. This analysis is based on v17.0.1-next.5).
Let's revisit the concept of input transforms:
You might be curious about the purpose of the function overload and the peculiar declaration at the bottom:
Firstly, if you provide only <ReadT>, the input transform is not available (similar to the current behavior with.
@Input
({ transform: ... })
Conversely, by passing both <ReadT, WriteT>, you gain the ability to include a transform. Keep in mind that:
ReadTdefines the type you use within your component.WriteTdefines the type that originates from external sources.
Here's another instance:
Returning to our intricate declaration 🥴, let's first concentrate on inputFunction:
It features triple overloads:
- When no initial value is provided, the inner type is extended with
undefined. - When an initial value is provided with only the
ReadTtype parameter (opts WITHOUT transform). - When an initial value is provided with both
ReadT, WriteTtype parameters (opts WITH transform).
This covers all the scenarios we've explored 😎💪
In summary, there are 3 overloads for the optional prop = input()
... and 2 overloads for the required prop = input.required().
That was quite a journey 😅 Hope you found it insightful.
Concluding Thoughts
We've examined numerous examples of how using input signals in different ways affects their underlying TypeScript types. The best approach depends on your unique needs for a given situation. Nevertheless, it's vital to always be mindful of the types that are declared or inferred, as type safety is a cornerstone of overall code quality.
While the Signal Input types are meticulously defined, the responsibility lies with you to determine whether an input should be optional or required in the long term. Types should mirror your design intentions.
Feel free to experiment with it on this stackblitz. It contains all the examples discussed.
Follow me on twitter for more in-depth explorations of frontend topics (js, ts, react, angular, etc.)!





















