Search code examples
angulartypescriptangular-pipe

Angular Pipe and TypeScript Type Guard


I read about type guards in typescript here and here. But I still get compiler errors.

Error:(21, 14) TS2349: Cannot invoke an expression whose type lacks a call signature. Type '{ (callbackfn: (value: Foo, index: number, array: Foo...' has no compatible call signatures.

I got following classes:

Foo.ts

export class Foo {
  expired: boolean;
}

Bar.ts

export class Bar {
  foo: Foo;
}

MyPipe.ts

import {Pipe, PipeTransform} from '@angular/core';

@Pipe({
  name: 'myPipe'
})
export class MyPipe implements PipeTransform {

  transform(items: Foo[] | Bar[], isExpired: Boolean): Foo[] | Bar[] {
    if (!items) {
      return items;
    }

    if (items[0] instanceof Foo) {
      return items.filter((foo: Foo) => {
        return foo.expired == isExpired;
      });
    } else {
      return items.filter((bar: Bar) => {
        return bar.foo.expired == isExpired;
      });
    }
  }
}

The question is, how can I achieve the use of union binding for my parameter "items" and the type guard usage at the same time in my angular pipe using typescript?


Solution

  • Typescript will generally not narrow the type of a variable based on the type of a field (with the exception of discriminated unions). More specifically typescript will do no narrowing based on array indexing (this is a known limitation)

    The simplest thing you can do is use a type assertion, or the more elegant solution, a custom type guard:

    class Foo { private x: string; expired: boolean }
    class Bar { private x: string; foo: Foo }
    
    function isArrayOf<T>(ctor: new (...args: any[]) => T, arr: any): arr is T[] {
        return arr[0] instanceof ctor
    }
    
    export class MyPipe {
        transform(items: Foo[] | Bar[], isExpired: Boolean): Foo[] | Bar[] {
            if (!items) {
                return items;
            }
    
            if (isArrayOf(Foo, items) {
                return items.filter((foo: Foo) => {
                    return foo.expired == isExpired;
                });
            } else {
                return items.filter((bar: Bar) => {
                    return bar.foo.expired == isExpired;
                });
            }
        }
    }
    

    Playground link