Diving into Type System behind Angular Signal Inputs

ducin

Tomasz Ducin

Posted on December 28, 2023

Diving into Type System behind Angular Signal Inputs

Original version of this writing has been published as a twitter thread. However, I was convinced by @eneajaho to convert it into a blogpost 😎. I decided to leave the original form: quick, easy-going and concise.


As you may know, Signal Inputs are coming to Angular in mid January 2024 in version 17.1. But we can actually play with them already, or at least, take a look at their almost public API. In this post we'll take a deep dive into the type design of Signal Inputs internals, its consequences, and a lot of examples of Signal Inputs usage.

Demo code

We're gonna expose what the Angular Team has hidden from us...

πŸͺ„ the trick is:

import { Ι΅input as input } from '@angular/core';
Enter fullscreen mode Exit fullscreen mode

don't do this at home 😈!

Following stackblitz allows you to see Signal Input declarations. Note that there's no Signal Input implementation within angular as of now (v17.1.0-next.5), so we'll focus on declarations/public API only.

ToC

πŸ› οΈ first - examples
πŸ₯© then - internals

We'll analyze the examples from above stackblitz one by one, and later we'll analyze the internals & types underneath.

Examples

We'll start with lots of examples. Take a look at the first one:

πŸ” initial value is used to infer the input's type, same as with ordinary signals:

signal usage screen

⚠️ Input Signals are READONLY, which makes sense, since it's the parent component who can put new values (via templates) into the input signal, not us πŸ˜‰

signal usage screen

One of my favorite design decisions:
πŸ’ͺ taking advantage of (1) #typescript's possibilities and (2) applying best solutions from other frameworks. In this case: from React

πŸ’œπŸ©·πŸ’œ love it πŸ©·πŸ’œπŸ©·

signal usage screen

Less exciting - and IMHO it should be avoided as much as possible (since it decreases readability and the understanding of how components exchange data) - aliasing is allowed:

πŸ“‘ aliased name is used by parent to pass the data
πŸ” the prop name is used internally (obviously)

signal usage screen

Another logical design decision:
πŸ’ͺ since an input is REQUIRED, passing initial value makes no sense πŸ˜‰ so it's removed from the signature

signal usage screen

Here the initialValue parameter is removed from the function (overload) signature. Or to be more precise - it's never added (never allowed) to be there πŸ˜‰

So, technically speaking, TypeScript would try to pass your initial as options object which obviously fails.

signal internals code

And one more example - transform is allowed only when passing both ReadT and WriteT type parameters. More on this later...

signal usage screen

Internals

Now let's see πŸ– some internals πŸ₯©

signal internals code

To get a broad overview of the direction, you might be interested in taking a look at the ➑️ Angular Team's Sub-RFC 3: Signal-based Components which deals with, more or less, how to make use of signals in Angular Components. Also, you can ➑️ take a look at the code itself. Anyway, let's dive into the internals!

We've got 2 new signal symbols:

signal internals code

it's the same usecase, as with the former unique Signal symbol - it allows to restrict compatibility across different (input) signals.

For instance:
βœ… one can assign the InputSignal<number, number> to an ordinary Signal<number>

signal internals code

Proof:

  1. need to make ordinary signal READONLY so that TS compares 2 readonly signals (input signals are READONLY)

  2. where a string signal is expected, a string input signal is acceptable, since it has all required (needed) properties.

  3. ❌ but the other way round it fails, since an ordinary string signal doesn't have the brand read/write signals.

signal compatibility proof code

Reassigning signals, however, is not going to be anyhow common πŸ˜‰. But the Angular compiler also makes use of the symbols internally.

Now, this is where we get the signals API from. There's a completely separate implementation for optional and required inputs. It's just exposed as a convenient API:

s1 = input()
s2 = input.required()
Enter fullscreen mode Exit fullscreen mode

signal internals code

The sad thing, however, is that currently we cannot see its runtime.

(disclaimer: as mentioned above, ng v17.1 is expected very soon, this analysis uses v17.0.1-next.5).

signal internals code

Now let's take a look back at input transform:

signal usage code

However, you might be wondering what the heck is going on with this function overload and the strange declaration at the bottom:

Image description

First of all, if we pass <ReadT> only, we can't pass the input transform (that's the same as with current

@Input
({ transform: ... })
Enter fullscreen mode Exit fullscreen mode

signal internals code

However, if we pass both <ReadT, WriteT>, we can additionally pass the transform. Note that:

  • ReadT is the expression type you want to use inside your component
  • WriteT is the expression type that is passed from outside

signal internals code

Another example:

signal usage code

Going back to our nice declaration πŸ₯΄ let's focus on inputFunction first:

signal internals code

It has 3 overloads on its own:

  • take no initial value and extend inner value with undefined
  • take the initial value with ReadT type param only (opts WITHOUT transform)
  • take the initial value with both ReadT, WriteT type params (opts WITH transform)

All usecases seen before 😎πŸ’ͺ

signal internals code

All in all we've got 3 overloads for the prop = input() (optional)
... and 2 overloads for the prop = input.required() (required)

signal internals code

Phew πŸ˜… that was quite a lot. Hope you enjoyed that.

Conclusion

We've seen quite a few examples on how certain usage of input signals affect underlying TypeScript types. Each usage depends on your specific needs in a specific situation. However, you should always pay attention to what types get declared/inferred in each case, as type-safety is one of the most important factors that form overall code quality.

Underlying Signal Input types are very well defined, but it's always your responsibility to verify, whether a given input should be optional or required in the long run. Types should only reflect your design.

Remember you can play with it on this stackblitz. It includes the code of all examples.

Follow me on twitter for more frontend (js, ts, react, angular etc.) deep dives!

πŸ’– πŸ’ͺ πŸ™… 🚩
ducin
Tomasz Ducin

Posted on December 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related