Search code examples
angulartypescript

Type guards using a computed signal?


Is it possible to use a computed signal to type guard within the template? I am using Angular 17.0.0 (17.1.0 is required for input signals, which we can't change for this version of the library).

I have two tests within this component, one using a signal isItem, and one using a function isItemFn.

  • isItem doesn't seem to allow me to typeguard this.item.
  • isItemFn does allow me to typeguard this.item.

Since the value of item can be one of three things (more to come later), I need a way to apply type guards in the template and I was hoping that signals would help but so far they have not. Is there a way to do this without using a function or a pipe?

@Component({})
export class CompositeChipsDisplayComponent<T> {
  @Input() item!: T;

  // Doesn't allow for type guards in the template
  isItem = computed(() => this.item instanceof CompositeChipsItemDirective);

  // Does allow for type guards in the template (but runs too often)
  isItemFn(item: unknown): item is CompositeChipsItemDirective {
    return item instanceof CompositeChipsItemDirective;
  }
}
<!-- Types work within the `if` statement when using a function -->
@if (isItemFn(item)) {
  <button class="item">
    {{ item.label }}
  </button>
}

<!-- Types do not work within the `if` statement when using a signal -->
@if (isItem()) {
  <button class="item">
    {{ item.label }}
  </button>
}

Here is what the output looks like in my editor:

Example output


Solution

  • The problems with your code are follows.

    1. You are using instanceof CompositeChipsItemDirective which converts the computed to a boolean signal, instead you should just return the signal after casting it to the directive type.

    2. You are using a computed in the HTML, but you are again using the original input inside the template accessing the label. Instead using the @if as syntax and use the type coming from the computed signal, and it should work fine.


    import { Component, computed, Directive, Input, Signal } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    
    @Directive({
      selector: '[someDirective]',
      standalone: true,
      exportAs: 'someDirective',
    })
    export class CompositeChipsItemDirective {
      label = 'test';
    }
    
    @Component({
      selector: 'app-child',
      standalone: true,
      template: `
        @if (isItemFn(item)) {
          <button class="item">
            {{ item.label }}
          </button>
        }
    
        @if (isItem(); as itemCasted) {
          <button class="item">
            {{ itemCasted.label }}
          </button>
        }
      `,
    })
    export class CompositeChipsDisplayComponent<T> {
      @Input() item!: T;
    
      // Doesn't allow for type guards in the template
      isItem: Signal<CompositeChipsItemDirective> = computed(
        () => return this.item instanceof CompositeChipsItemDirective ? this.item as CompositeChipsItemDirective : null;
      );
    
      // Does allow for type guards in the template (but runs too often)
      isItemFn(item: unknown): item is CompositeChipsItemDirective {
        return item instanceof CompositeChipsItemDirective;
      }
    }
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [CompositeChipsDisplayComponent, CompositeChipsItemDirective],
      template: `
        <app-child [item]="test"/>
        <div #test="someDirective" someDirective></div>
      `,
    })
    export class App {
      name = 'Angular';
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo