Skip to content

'identity' modifier to indicate a function's parameter-less returns should be narrowed like a valueΒ #60948

Open

Description

πŸ” Search Terms

indicate function returns identical value cached memoized signals

βœ… Viability Checklist

⭐ Suggestion

Many apps today are built on the concept of using small "getter" functions as wrappers around values. For example, Signals as implemented in Angular, Solid, and the stage 1 TC39 Signals proposal often look something like:

declare const value: () => string | undefined;

if (value() !== undefined) {
  console.log(value().toUpperCase());
}

Signals users have struggled with using them in TypeScript because, at present, there isn't a way to get that code block to type check without type errors. Signals users know that the result of value() must be string inside the if, but TypeScript doesn't have a way to note that the result should be type narrowed. Common workarounds today include !, ?., and refactoring to store intermediate values. All of which are at best unnecessary verbosity, and at worst conflict with frameworks.

Request: can we have a keyword -or, failing that, built-in / intrinsic type- to indicate that calls to a function produce a referentially equal, structurally unchanging value? In other words, that the function call (value()) should be treated by type narrowing as if it was just a variable reference (value)?

Proposal: how about an identity modifier keyword for function types that goes before the ()? It would be treated in syntax space similarly to other modifier keywords such as abstract and readonly.

πŸ“ƒ Motivating Example

When an identity function is called, it is given the same type narrowing as variables. Code like this would now type check without type errors, as if value was declared as const value: string | undefined:

declare const value: identity () => string | undefined;

if (value() !== undefined) {
  value();
  // Before: string | undefined
  // Now: string

  console.log(value().toUpperCase());
  // Before:  ~~~~~~~ Object is possibly 'undefined'.
  // Now: βœ…
}

Narrowing would be cleared the same as variables when, say, a new closure/scope can't be guaranteed to preserve narrowing:

declare const value: identity () => string | undefined;

if (value() !== undefined) {
  setTimeout(() => {
    value();
    // Still: string | undefined

    console.log(value().toUpperCase());
    // Still:   ~~~~~~~ Object is possibly 'undefined'.
  });
}

πŸ’» Use Cases

One difficult-to-answer design question is: how could identity handle functions with parameters? I propose the modifier not be allowed on function signaturess with parameters to start. It should produce a type error for now. The vast majority of Signals users wouldn't need signatures with parameters, so I don't think solidifying that needs to block this proposal. IMO that can always be worked on later.

Furthermore, it's common for frameworks to set up functions with a parameter-less "getter" signature and a single-parameter "setter" signature. I propose for an initial version of the feature, calling any other methods or setting to any properties on the type should clear type narrowing:

declare const value: {
  identity (): string | undefined;
  (newValue: string | undefined): void;
}

if (value() !== undefined) {
  value("...");

  value();
  // Still: string | undefined

  console.log(value().toUpperCase());
  // Still:   ~~~~~~~ Object is possibly 'undefined'.
}

More details on the difficulties of signals with TypeScript:

If a new modifier keyword isn't palatable, a fallback proposal could be a built-in type like Identity<T>. This wouldn't be a new utility type (FAQ: no new utility types); it'd be closer to the built-in template string manipulation types.

Activity

  1. fabiospampinato commented on Jan 10, 2025

    @fabiospampinato

    Keeping the narrowing inside the if seems problematic if any function could be executing before our second call to value() here, as that could result in the type checker telling us something incorrect:

    declare const value: () => string | undefined;
    
    if (value() !== undefined) {
      whatever();
      console.log(value().toUpperCase());
    }

    Also this working only in getter/setter-style signals is potentially limiting, for example I use unified signals, with a single function that is both a getter and a setter, because there are many benefits to doing that in a typed codebase (which essentially prevents the common issues of this approach from happening), and if this worked only for getters it would be of no use to me, basically.

    In general the deeper problem seems about somehow detecting when various types of narrowing should be invalidated, which seems very hard to express and very difficult for the type checker to check for.

    Worth exploring this area though, as signals are getting more popular.

  2. Andarist commented on Jan 10, 2025

    @Andarist
    Contributor

    Keeping the narrowing inside the if seems problematic if any function could be executing before our second call to value() here, as that could result in the type checker telling us something incorrect:

    The same already happens with property narrowing so this wouldn't be a new problem at all. This would just use the same tradeoffs as the ones mentioned in #9998

  3. RyanCavanaugh commented on Jan 10, 2025

    @RyanCavanaugh
    Member

    Isn't the core problem in this example that value is incorrectly declared? If it were

    declare const value: (() => string) | (() => undefined);
    
    if (value() !== undefined) {
      console.log(value().toUpperCase());
    }

    Then there's a very straightforward path to adding a narrowing rule that allows a normal interpretation of narrowing value to the () => string constituent.

  4. RyanCavanaugh commented on Jan 10, 2025

    @RyanCavanaugh
    Member

    Also an ELI5 explanation for why it's not correct to write const v = value(); would be appreciated

  5. alxhub commented on Jan 10, 2025

    @alxhub

    Isn't the core problem in this example that value is incorrectly declared?

    Such a type might work for the simple case, but isn't really generalizable. If you consider the most basic implementation of a signal-like type:

    class Signal<T> {
      constructor(private value: T) {}
    
      get(): T { return this.value; }
      set(value: T): void { this.value = value }
    }

    it would be very difficult / infeasible to type get into a union of all possible function types expressing the different narrowed shapes of T, without knowing the specific T. This is also a combinatorial problem: if T is something like {id: string|number, value: Data|undefined}, both id and value can be narrowed independently and encoding that into the get type would result in 4 different function types (assuming that even works).

    Also an ELI5 explanation for why it's not correct to write const v = value(); would be appreciated

    It's overhead compared to the experience with plain properties. Sometimes you have multiple levels of operations or multiple reads, which would result in a proliferation of temporary variables.

    More critically, while value() is stable in the sense that repeated calls will return the same value, it may not be side-effect free. In the following chain:

    if (x()) {
      createNewContext();
      x().value;
    }

    it might be important to record that x() is invoked after createNewContext(). Extracting to a temporary variable fixes the narrowing, but may alter the behavior in a breaking way.

  6. RyanCavanaugh commented on Jan 10, 2025

    @RyanCavanaugh
    Member

    A distributive conditional type would be correct, though you would only be able to narrow a union (string | number), not a specific value (if T is string you still couldn't narrow to 0)

    type PossibleFuncs<T> = T extends unknown ? () => T : never;
    declare class Signal<T> {
      constructor(value: T) {}
    
      get: PossibleFuncs<T>
      set(value: T): void;
    }
  7. ryansolid commented on Jan 10, 2025

    @ryansolid

    Since where the read(function execution) happens matters in Signals libraries hoisting is incredibly clunky for a lot of cases. Especially in templating. Like think of JSX where everything is an expression not a statement. Most templating languages are effectively similar. Signal libraries tend to be granular in their rendering so components/templates don't re-run on a whole. Only parts that change re-execute. So the Signal function needs to be accessed in a very specific scope to trigger the right execution.

    Not sure of people's familiarity but this is why often these sort of libraries can't destructure props. Because you can't access the getters at the top of the component but instead in the expression closest to where they are used. Different problem but part of the same mechanisms that are present here.

    You don't always have a place to define variables and access the signal that might need to be used way nested down in your template. Places that are conditionally rendered, or parts of loops. A map function atleast can be made a block statement to be fair but even inside it there will be nested expressions so it can become onerous.

    This is a fundamental aspect of Signals and the more granular people leverage them the more inevitable it will come that it will be painful to try to hoist stuff.

  8. Monkatraz commented on Jan 10, 2025

    @Monkatraz

    A distributive conditional type would be correct, though you would only be able to narrow a union (string | number), not a specific value (if T is string you still couldn't narrow to 0)

    type PossibleFuncs = T extends unknown ? () => T : never;
    declare class Signal {
    constructor(value: T) {}

    get: PossibleFuncs
    set(value: T): void;
    }

    How do you actually write this in non-declaration code?

    This fails to type, and doesn't actually put get on the prototype:

    type PossibleFuncs<T> = T extends unknown ? () => T : never;
    
    class Signal<T> {
      private value: T;
    
      constructor(value: T) {
        this.value = value;
      }
    
      get: PossibleFuncs<T> = () => {
        return this.value;
      }
    
      set(value: T): void {
        this.value = value;
      }
    }

    You can do this instead, but value is considered unused, and it looks a little absurd:

    type PossibleFuncs<T> = T extends unknown ? () => T : never;
    
    interface Signal<T> {
      get: PossibleFuncs<T>;
    }
    
    class Signal<T> {
      private value: T;
    
      constructor(value: T) {
        this.value = value;
      }
    
      set(value: T): void {
        this.value = value;
      }
    }
    
    Signal.prototype.get = function () {
      return this.value;
    }

    As a side-note, Solid calls these kinds of functions Accessors

  9. alxhub commented on Jan 10, 2025

    @alxhub

    A distributive conditional type would be correct, though you would only be able to narrow a union (string | number), not a specific value (if T is string you still couldn't narrow to 0)

    This also only works for types in the top-level union. If a property of T is itself a union, it can't be narrowed via selecting a specific variant of PossibleFuncs<T>.

  10. RyanCavanaugh commented on Jan 10, 2025

    @RyanCavanaugh
    Member

    Wait, if the idea is that get "always returns the same value", why is there a set method in the first place? Doesn't this imply the need for additional syntax to identify which functions/methods invalidate the narrowing?

  11. Monkatraz commented on Jan 10, 2025

    @Monkatraz

    Wait, if the idea is that get "always returns the same value", why is there a set method in the first place? Doesn't this imply the need for additional syntax to identify which functions/methods invalidate the narrowing?

    I mean, this violates the spirit of your argument as well:

    class Foo {
      value: number | null = null
    
      reset() {
        this.value = null
      }
    }
    
    const foo = new Foo()
    foo.value = 123
    foo.reset()
    
    // not a type error
    foo.value.toFixed()
  12. Monkatraz commented on Jan 10, 2025

    @Monkatraz

    I suppose if you're building with explicitly TypeScript in mind the ideal implementation is something like this:

    class Signal<T> {
      private _value: T;
    
      constructor(value: T) {
        this._value = value;
      }
    
      get value(): T {
        // ... signal tracking logic
    
        return this._value;
      }
    
      set value(value: T) {
        // ... signal tracking logic
    
        this._value = value;
      }
    }

    But this wouldn't work for e.g. Solid, which just has accessors like foo() with no property access. Also, most consumers of the signal library won't be given signals directly and will instead have something wrapped around them, like useState in React or createSignal in Solid, both of which do not return an object that you do property access on.

    EDIT: Also, the explicit function call syntax (so signal.get()) gets across that the access actually has side effects a little better.

  13. RyanCavanaugh commented on Jan 10, 2025

    @RyanCavanaugh
    Member

    I mean, this violates the spirit of your argument as well:

    Okay, but it seems extremely nearsighted to ship a new CFA feature that is going to immediately run into another feature request before it's considered useful. Bringing up the current CFA trade-offs as relates to properties doesn't seem like a useful way to address this concern.

  14. Monkatraz commented on Jan 10, 2025

    @Monkatraz

    Okay, but it seems extremely nearsighted to ship a new CFA feature that is going to immediately run into another feature request before it's considered useful. Bringing up the current CFA trade-offs as relates to properties doesn't seem like a useful way to address this concern.

    You're not wrong, but I think the feature would still be helpful regardless. The trade-offs are a "known quantity" to me, and this doesn't introduce anymore of them, right? It would be basically identical to how getters behave (except with no setter, I guess)

    EDIT: I think with this feature it would be important for the naming and docs to get across it's intended for accessors, so as long as you don't yield to anything, you will know what the value is and it can't change on you (...usually), like a property.

  15. Monkatraz commented on Jan 10, 2025

    @Monkatraz

    As an interesting anecdote, Svelte's old style of stores basically handled this problem via the magic $store syntax:

    <script>
      import { writable } from 'svelte/store';
    
      const value = writable<number | null>(null)
    
      // magic reactive store access
      if ($value !== null) {
        // $value is now typed as non-null
        console.log($value.toFixed())
      }
    </script>

    Kind of like Signals, Svelte's compiler would track where you used these magic accesses. The Svelte LSP would basically trick the TypeScript LSP into thinking $value was a real variable iirc, and so it would naturally handle narrowing. It would even handle you setting the store's value, which I'm not sure will ever be achievable with any kind of [get, set] = whatever() pattern without some pretty substantial additions to TypeScript.

    EDIT: There is also an old references proposal for JS which also touches on this problem, specifically this issue about making the concept of references extensible. Food for thought.

  16. 11 remaining items

  17. shicks commented on Jan 17, 2025

    @shicks
    Contributor

    Good points above about how mutating () => void is kinda meaningless, but the example of

    interface Signal<T> {
      stable (): T; // NOTE: how to differentiate this from a _method_ named "stable"?
    }

    (which I think is necessary) would suggest that stable () => T is probably a meaningful type on its own, which would just never be invalidated once it's torn off.

    Other interesting use cases to consider:

    1. Google's (apparently erstwhile) official JS protobuffer implementation uses getter functions, which would be nice to properly narrow: if (message.getFoo()) useNonNull(message.getFoo());
    2. Lazy evaluation often indirects via a function - if the lazily-computed value might actually really be missing, then it could have type stable () => Foo|undefined.
  18. robbiespeed commented on Jan 18, 2025

    @robbiespeed

    I'm really not sure about this proposal, there's a 50/50 near 0% chance I'd use it in my signals lib. Technically it makes things less type safe which I do not like at all. It improves DX in a lot of cases, but at the risk of making it much more confusing/bug prone in other cases.

    Example of a confusing case that could be quite common:

    interface MutableSignal<T> {
      stable get(): T;
      mutating set(v: T): undefined;
    }
    interface Signal<T> {
      stable get(): T;
    }
    
    const a: MutableSignal<string> = state("hello");
    const b: Signal<string> = derived(() => a.get().toUpperCase());
    
    if (b.get() === "HELLO") {
      a.set("world");
      b.get(); // BUG: type is still narrowed to "HELLO" despite the value changing to "WORLD"
    }

    Signals are built for the purpose of composing state derivation. Getters have the same issue, but I don't think it would be as commonly encountered.

    Further narrowing after a dependency has mutated would also become very problematic.

    if (b.get() === "HELLO") {
      a.set("world");
      if (b.get() === "WORLD") {
        // ^ This comparison appears to be unintentional because the types '"HELLO"' and '"WORLD"' have no overlap.(2367)
      }
    }

    Even with casting to allow the condition the narrowed type never changes.

    if (b.get() === "HELLO") {
      a.set("world");
      if ((b as Signal<"WORLD">).get() === "WORLD") {
        b.get(); // WRONG type "HELLO"
      }
    }

    There is a work around but most users are not going to know how to do it.
    Originally thought this was a work around, but no it just makes the type never.

    function assertType <T>(v: any): asserts v is T {}
    
    if (b.get() === "HELLO") {
      a.set("world");
      assertType<Signal<"WORLD">>(b);
      if (b.get()  === "WORLD") {
        b.get(); // WRONG type never
      }
    }

    These aren't new problems, they exist with getters (TS playground). However I do think it's far more likely to run into the issue with signals.

    I'd much rather encourage users to assign the value to a local variable:

    let bVal = b.get();
    if (bVal === "HELLO") {
      a.set("world");
      bVal = b.get();
      if (bVal  === "WORLD") {
        bVal; // CORRECT type "WORLD"
      }
    }

    Kinda wish there was a strict option to disable property access narrowing, and if this lands I'd defiantly want a way to disable it for my own projects.

  19. ryansolid commented on Jan 19, 2025

    @ryansolid

    I understand being tooling authors/maintainers we are accustomed to thinking about edge cases. Because let's face it, if we make something possible people will abuse it.

    stable is significantly more important than mutating. I get why mutating is challenging, because the way the reactive graph works a change of one thing could invalidate others and there is no reasonable way for TS to know that. I'd take mutating become the nuclear option(ie, invalidates all stable) before giving up stable.

    The reason is with Signals mutation for the most part(if not always) should be limited to specific scopes. Pure computation(derivations) shouldn't mutate. This limits it to event handlers, and denoted effectful portions of the code. Code that is effectively imperative. Code where block statements exist and reactive scope does not matter. Not only is this a much smaller fraction of the code(like 10% maybe) but it is place where you can set a local variable.

    But the other 90% of the code where there is no mutation, where templating logic lives, where primitives compose, where you build the graph, and where assigning local variables is unreasonable benefit immensely from stable.

    I'm also fine with the current proposal because we are talking about such a small slice of a slice. It is consistent with getter/setters today and atleast this way it is explicit and can apply to other functions.

  20. robbiespeed commented on Jan 19, 2025

    @robbiespeed

    Ryan Carniato (@ryansolid) Can you give an example outside of templates where assigning to a local variable is unreasonable? I do not think we should be creating a feature which has it's primary use case tailored to non standard template syntax, where something like a TS language service plugin could do the work.

    My concern is this changes the failure mode of signals in block scope reactive areas from a non narrowed type safe annoyance, to one which reduces type safety and can introduces runtime errors. Users are unlikely to know TS is producing incorrect types from overly optimistic narrowing, so they wouldn't do the variable assignment to workaround.

    Not all signals libs have restriction about reactive scopes, in some it's totally fine to mutate signals anywhere even inside derivations, and reactivity can remain stable and active. Even still just considering event handler contexts I think this would be enough of a footgun there.

    This change has the potential to steer signal libs into queuing reactive propagation till after event handlers, since that would remove the type safety issue of mutations affecting derivations.

    #57725 (pure/impure fn/accessor distinction) has the potential to allow a safe way for narrowing signals, and improve the safety of getter/prop narrowing.

  21. fardolieri commented on Jan 19, 2025

    @fardolieri

    Wouldn't Ryan's suggestion (using function unions) address most of the stable problem?

    declare const value: (() => string) | (() => undefined);
    
    if (value() !== undefined) {
      console.log(value().toUpperCase());
    }

    The mutating part is the tricky one. Has anyone explored the idea of extending type predicates or type guards to allow changing properties of this?

    type Signal<T> = {
        value: () => T | undefined,
        setValue: (value: T) => this.value is () => T, // Calling this function removes undefined from 'value'
    }
  22. shicks commented on Jan 19, 2025

    @shicks
    Contributor

    Nuclear option for mutating sounds like it might be the most reasonable option, and would alleviate many of the concerns about mutating signals, given how common derived signals are likely to be. That said, there are tons of instances where type narrowing chooses ergonomics over soundness. For example, the fact that array access is potentially undefined, bivariance on overridden method signatures, or type narrowing on nested properties. These "not 100% stable" concerns seem to be of roughly similar consequence.

  23. alxhub commented on Jan 20, 2025

    @alxhub

    That said, there are tons of instances where type narrowing chooses ergonomics over soundness. For example, the fact that array access is potentially undefined, bivariance on overridden method signatures, or type narrowing on nested properties. These "not 100% stable" concerns seem to be of roughly similar consequence.

    This is generally how we see this feature. It's always possible to construct examples where TypeScript's soundness falters, but our experience with signals so far mirrors Ryan Carniato (@ryansolid)'s - this tends not to be an issue in everyday code.

    Re: Robbie Speed (@robbiespeed)

    I do not think we should be creating a feature which has it's primary use case tailored to non standard template syntax, where something like a TS language service plugin could do the work.

    For what it's worth, we explored the idea of implementing narrowing as a feature of our language service pretty extensively, and concluded that it was infeasible. The only workable way to achieve the narrowing semantics is to transform incoming expressions and extract the result of stable function calls into temporary variables. This assumes that you can know which of those function calls are stable (are signal reads), which is already not possible without running through type-checking already. We were even willing to treat all zero-arg calls as stable, but it turns out this variable extraction is difficult to impossible to do while preserving all the other type flow operations and semantics.

    Additionally, we generally want TypeScript to behave the same inside and outside of templates - narrowing within computed signals is just as important as narrowing within templates.

  24. dummdidumm commented on Jan 20, 2025

    @dummdidumm

    I argee that stable on its own makes sense, too. For example if you only pass the getter function into another function.

    function foo(getValue: stable () => null | Foo) {
      return derived(() => {
        if (getValue()) {
           getValue(); // should be of type Foo
        }
      });
    }
  25. c-harding commented on Feb 4, 2025

    @c-harding

    Can you give an example outside of templates where assigning to a local variable is unreasonable? I do not think we should be creating a feature which has it's primary use case tailored to non standard template syntax, where something like a TS language service plugin could do the work.

    Robbie Speed (@robbiespeed) for me the big one is with one-liner functions, such as with the computed function implemented by Vue and Angular.

    Vue’s solution, where reading a ref is done by getter, typechecks:

    declare function kebabCase(name: string): string;
    
    const person: Ref<{ name: string } | undefined> = ref<{ name: string }>();
    
    const userNameSuggestion: Readonly<Ref<string | undefined>> = computed(() => person.value && kebabCase(person.value.name));

    Angular’s solution, where reading a signal is done by function, does not typecheck:

    declare function kebabCase(name: string): string;
    
    const person: WritableSignal<{ name: string } | undefined> = signal<{ name: string } | undefined>(undefined);
    
    const userNameSuggestion: Signal<string | undefined> = computed(() => person() && kebabCase(person().name));

    To rewrite this to typecheck, it would have to be:

    const userNameSuggestion: Signal<string | undefined> = computed(() => {
      const p = person();
      return p && kebabCase(p.name);
    });

    or

    const userNameSuggestion: Signal<string | undefined> = computed(() => {
      const p = person();
      if (p) return kebabCase(p.name);
      else return undefined;
    });
  26. andreialecu commented on Feb 5, 2025

    @andreialecu

    A case I haven’t seen mentioned that could introduce complexity, potentially from a performance perspective as well:

    interface Signal<T> {
      stable(): T;
      mutating set(value: T): void;
    }
    
    declare function signal<T>(): Signal<T>;
    
    class Foo {
      val = signal<string | undefined>();
    
      someMethod() {
        if (this.val()) {
          this.doBar();
          // The value should become `undefined` here again
          console.log(this.val().toUpperCase());
        }
      }
    
      doBar() {
        if (this.coinflip()) {
          // This would need to reset the value
          this.val.set(undefined);
        }
      }
    
      coinflip() {
        return Math.random() > 0.5;
      }
    }

    This can easily lead to random function calls creating TS errors without an obvious reason, making debugging difficult.

    That said, I’d really like to see some improved DX around this scenario, and I hope a solution is found.

    A feature like assignment in a conditional expression (similar to what C++, Swift, or even PHP supports) might have helped here:

    // c++
    if (int a = foo(); a != 0) {
      //...
    }
    if (const val = this.signal()) {
      ...
    }
  27. JeanMeche commented on Feb 5, 2025

    @JeanMeche
    Contributor

    Typescript is not intended to create new syntactic sugar.

    Those are developed at the platform level, see https://github.com/tc39/proposal-Declarations-in-Conditionals.

  28. andreialecu commented on Feb 5, 2025

    @andreialecu

    Typescript is not intended to create new syntactic sugar.

    Those are developed at the platform level, see tc39/proposal-Declarations-in-Conditionals.

    I know; I was mainly pointing out in that direction as a possible solution.

    It may be time to revive that proposal and fast-track it. πŸ™‚

  29. shicks commented on Feb 5, 2025

    @shicks
    Contributor

    Andrei Alecu (@andreialecu) wrote:

    A case I haven’t seen mentioned that could introduce complexity, potentially from a performance perspective as well:

    class Foo {
      val = signal<string | undefined>();
    
      someMethod() {
        if (this.val()) {
          this.doBar();
          // The value should become `undefined` here again
          console.log(this.val().toUpperCase());
        }
      }
    
      doBar() {
        if (this.coinflip()) {
          // This would need to reset the value
          this.val.set(undefined);
        }
      }

    This can easily lead to random function calls creating TS errors without an obvious reason, making debugging difficult.

    That said, I’d really like to see some improved DX around this scenario, and I hope a solution is found.

    To make this work well, I think doBar would need to be locally/explicitly tagged as mutating (rather than expecting the type checker to understand the transitive behavior automatically), which further suggests that mutating probably wants to be global rather than tracked per-signal, since otherwise you get into a morass of specifying "what does this mutate?" Ideally you'd also want a check that mutating is viral: calling the mutating this.val.set from a non-mutating doBar would be an error, though pragmatically I suspect this would make any new mutating annotations nearly-impossible to land; I don't know what the sweet spot would be.

  30. JeanMeche commented on Mar 11, 2025

    @JeanMeche
    Contributor

    Daniel Rosenwasser (@DanielRosenwasser) What would be the next step for the dicussion ?

    Do you still expect more feedback ?

  31. csvn commented on Oct 1, 2026

    @csvn

    Been pretty quiet here for a year. Has there been any internal discussion on this issue?

    Just thought of this again when using a common pattern with signals, that can't be narrowed correctly due to param-less getter function:

    @switch (signalValue()) {
      @case (1) { One }
      @case (2) { Two }
      @default never; // signalValue() is not narrowed, so compiler error fails build
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs More InfoThe issue still hasn't been fully clarified

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions