tc39 / proposal-signals

A proposal to add signals to JavaScript.
MIT License
3.32k stars 57 forks source link

Refactor Signal Class Hierarchy for Improved Type Safety and Extensibility #230

Open DeepDoge opened 3 months ago

DeepDoge commented 3 months ago

Currently, State and Computed are defined as standalone classes within the Signal namespace:

interface Signal<T> {}
namespace Signal {
  class State<T> {}
  class Computed<T> {}
}

I suggest a refactor where State and Computed extend a base Signal class:

class Signal<T> {}
namespace Signal {
  class State<T> extends Signal<T> {}
  class Computed<T> extends Signal<T> {}
}

With this structure, we can perform type checks using a single base class:

signalOrValue instanceof Signal;

This is more straightforward and efficient than building a custom type guard function like:

function isSignal(value: unknown): value is Signal

The base Signal class can be read-only, while State, extending Signal, can expose the set() method, and Computed can set() the signal internally. So when I say signalOrValue instanceof Signal I don't care about if it's a State or Computed, i just know its something that can change and I can watch those changes.

Of course I might be missing something here, in that case I would like to know why?

NullVoxPopuli commented 3 months ago

I can't speak for everyone here, but I'm a big fan of this proposed type change. 🎉

I think we have some subclassing tests in the polyfill (but if we don't, we should add them).

Could be worth a PR to try it out and see what happens and then post the results back here? (for me, it's usually easier to discuss things with concrete code -- cause everything is too complicated)

https://github.com/proposal-signals/signal-polyfill

shaylew commented 3 months ago

I think the question is: what happens if you subclass Signal directly? Does its constructor simply throw, except when constructing a Computed or State?

I'm trying to remember what other precedents there are in the standard. The base TypedArray doesn't have a global name (though you can still get it as the prototype of the concrete typed array classes); are there cases where an abstract base class is exposed?

IMO this may be a reason to come up with some more general thing that's interoperable with signals and which State and Computed are special cases of (with a more convenient API and likely their own tuning in implementations), but absent the capability to subclass or instantiate Signal I'm not as sure about exposing it as a class.